@@ -43,6 +43,15 @@ function numsize($size, $round = 2)
4343 return round ($ size / pow (1000 , ($ i = floor (log ($ size , 1000 )))), $ round ) . $ unit [$ i ];
4444}
4545
46+ function safe_utf8 (string $ input ): string
47+ {
48+ // Ensure JSON output is valid UTF-8.
49+ // iconv is commonly available; if it fails, fall back to stripping invalid bytes.
50+ $ converted = @iconv ('UTF-8 ' , 'UTF-8//IGNORE ' , $ input );
51+ if ($ converted !== false ) return $ converted ;
52+ return preg_replace ('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u ' , '' , $ input ) ?? '' ;
53+ }
54+
4655// fix whitespace in path results in not found errors
4756$ request_uri = rawurldecode (parse_url ($ _SERVER ['REQUEST_URI ' ], PHP_URL_PATH ));
4857
@@ -1092,8 +1101,11 @@ function toggletheme() {
10921101 </div>
10931102 </div>
10941103 <div class="modal-footer">
1095- $ {{`process.env.API === "true" ? '<a id="file-info-url-api" href="" type="button" class="btn rounded btn-secondary" data-bs-dismiss="modal">API <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-code"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 8l-4 4l4 4" /><path d="M17 8l4 4l-4 4" /><path d="M14 4l-4 16" /></svg></a>' : '' `}}$
1096- <a id="file-info-url" type="button" class="btn rounded btn-primary">Download <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-download"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2" /><path d="M7 11l5 5l5 -5" /><path d="M12 4l0 12" /></svg></a>
1104+ <!-- TODO: add copy button if kind == text -->
1105+ <button id="file-popup-copy" type="button" class="btn rounded btn-secondary" disabled><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-copy"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z" /><path d="M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1" /></svg> Copy Text</button>
1106+
1107+ $ {{`process.env.API === "true" ? '<a id="file-info-url-api" href="?info" target="_blank" type="button" class="btn rounded btn-secondary"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-code"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 8l-4 4l4 4" /><path d="M17 8l4 4l-4 4" /><path d="M14 4l-4 16" /></svg> API</a>' : '' `}}$
1108+ <a id="file-info-url" type="button" class="btn rounded btn-primary"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-download"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-2" /><path d="M7 11l5 5l5 -5" /><path d="M12 4l0 12" /></svg> Download</a>
10971109 </div>
10981110 </div>
10991111 </div>
@@ -1102,6 +1114,38 @@ function toggletheme() {
11021114
11031115 <!-- Powered by https://github.com/adrianschubek/dir-browser -->
11041116 <script data-turbo-eval="false">
1117+ const copyTextToClipboard = async (text) => {
1118+ if (typeof text !== 'string' || text.length === 0) return false;
1119+
1120+ // Prefer async clipboard API when available (requires secure context).
1121+ try {
1122+ if (navigator.clipboard && window.isSecureContext) {
1123+ await navigator.clipboard.writeText(text);
1124+ return true;
1125+ }
1126+ } catch (e) {
1127+ // Fall back below.
1128+ }
1129+
1130+ // HTTP / non-secure fallback using execCommand.
1131+ try {
1132+ const textarea = document.createElement('textarea');
1133+ textarea.value = text;
1134+ textarea.setAttribute('readonly', '');
1135+ textarea.style.position = 'fixed';
1136+ textarea.style.top = '-1000px';
1137+ textarea.style.left = '-1000px';
1138+ document.body.appendChild(textarea);
1139+ textarea.select();
1140+ textarea.setSelectionRange(0, textarea.value.length);
1141+ const ok = document.execCommand('copy');
1142+ document.body.removeChild(textarea);
1143+ return ok;
1144+ } catch (e) {
1145+ return false;
1146+ }
1147+ };
1148+
11051149 $[if `process.env.DATE_FORMAT === "relative " `]$
11061150 function getRelativeTimeString(date, lang = navigator.language) {
11071151 const timeMs = typeof date === "number" ? date : date.getTime();
@@ -1133,11 +1177,11 @@ function getRelativeTimeString(date, lang = navigator.language) {
11331177 $[if `process.env.HASH === "true " `]$
11341178 // via api bc otherwise we need to include the hash in the tree itself which is costly
11351179 const getHashViaApi = async (url) => {
1136- await fetch(url)
1137- .then(response => response.json())
1138- .then( data => {
1139- navigator.clipboard.writeText( data.hash_ $ {{`process.env.HASH_ALGO `}}$) ;
1140- } );
1180+ const res = await fetch(url);
1181+ if (!res.ok) throw new Error('Hash request failed');
1182+ const data = await res.json();
1183+ const hash = data.hash_ $ {{`process.env.HASH_ALGO `}}$;
1184+ await copyTextToClipboard(String(hash ?? '') );
11411185 }
11421186 $[end]$
11431187
@@ -1542,7 +1586,18 @@ function getRelativeTimeString(date, lang = navigator.language) {
15421586 updateMultiselect((localStorage.getItem("multiSelectMode") ?? false) === "true");
15431587 $[end]$
15441588
1545- $[if `process.env.LAYOUT === "popup " || process.env.LAYOUT === "full " `]$
1589+ $[if `process.env.LAYOUT === "popup " `]$
1590+ (() => {
1591+ const copyBtn = document.querySelector('#file-popup-copy');
1592+ if (copyBtn && copyBtn.dataset.dbBoundClick !== '1') {
1593+ copyBtn.dataset.dbBoundClick = '1';
1594+ copyBtn.addEventListener('click', async (e) => {
1595+ e.preventDefault();
1596+ await copyPreviewToClipboard();
1597+ });
1598+ }
1599+ })();
1600+
15461601 document.querySelectorAll('.db-file').forEach((item) => {
15471602 // skip folders
15481603 if (item.getAttribute('data-file-isdir') === '1') {
0 commit comments