Upload files to "/"
This commit is contained in:
parent
bc03c6b514
commit
88ea01e723
1 changed files with 209 additions and 0 deletions
209
extensions.php
Normal file
209
extensions.php
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Optional add-ons for print.php. Keep this file next to print.php (index.php).
|
||||||
|
* Delete it and the portal still works; set any switch below to false to turn a feature off.
|
||||||
|
*
|
||||||
|
* Needs for Word/Excel/PowerPoint: sudo apt install libreoffice-writer libreoffice-calc libreoffice-impress
|
||||||
|
* Needs for preview: pdf.js files in ./vendor/pdfjs/ (see the setup commands). Without them,
|
||||||
|
* preview falls back to opening the file in a new browser tab.
|
||||||
|
*/
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
const EXT_OFFICE = true; // convert Word/Excel/PowerPoint/OpenDocument files to PDF, then print
|
||||||
|
const EXT_OFFICE_TYPES = ['docx', 'doc', 'odt', 'rtf', 'xlsx', 'xls', 'ods', 'pptx', 'ppt', 'odp'];
|
||||||
|
const EXT_SOFFICE = '/usr/bin/libreoffice';
|
||||||
|
const EXT_TIMEOUT = 90; // seconds allowed per conversion
|
||||||
|
const EXT_PREVIEW = true; // in-page preview (PDF, photos, text) before printing
|
||||||
|
const EXT_VENDOR = __DIR__ . '/vendor/pdfjs';
|
||||||
|
const EXT_PAGES_PER_SHEET = true; // "Pages per sheet" option (saves paper)
|
||||||
|
|
||||||
|
function ext_boot(): void {
|
||||||
|
if (!EXT_PREVIEW) { return; }
|
||||||
|
// Widens the page policy just enough for the bundled PDF viewer. Keep in step with print.php.
|
||||||
|
// header("Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'self'");
|
||||||
|
if (isset($_GET['view'])) { ext_serve((string)$_GET['view']); }
|
||||||
|
if (isset($_GET['asset'])) { ext_asset((string)$_GET['asset']); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function ext_accept(): string { return EXT_OFFICE ? ',.' . implode(',.', EXT_OFFICE_TYPES) : ''; }
|
||||||
|
function ext_hint(): string { return EXT_OFFICE ? ', Word, Excel, slides' : ''; }
|
||||||
|
|
||||||
|
function ext_form_html(): string {
|
||||||
|
if (!EXT_PAGES_PER_SHEET) { return ''; }
|
||||||
|
$opts = '';
|
||||||
|
foreach ([1 => '1 (normal)', 2 => '2', 4 => '4', 6 => '6', 9 => '9'] as $n => $label) {
|
||||||
|
$opts .= '<option value="' . $n . '">' . $label . '</option>';
|
||||||
|
}
|
||||||
|
return '<label class="f">Pages per sheet<select name="nup">' . $opts . '</select></label>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function ext_lp_args(string $ext): array {
|
||||||
|
$n = (int)($_POST['nup'] ?? 1);
|
||||||
|
return (EXT_PAGES_PER_SHEET && in_array($n, [2, 4, 6, 9], true)) ? ['-o', 'number-up=' . $n] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ext_file_html(string $name): string {
|
||||||
|
if (!EXT_PREVIEW) { return ''; }
|
||||||
|
return '<a class="mini" href="?view=' . rawurlencode($name) . '" data-preview="' . htmlspecialchars($name, ENT_QUOTES) . '" target="_blank" rel="noopener">Preview</a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called for every upload. Returns null to leave the file alone, ['error' => msg] on failure,
|
||||||
|
* or ['path' => converted pdf, 'name' => new filename] after converting an Office file.
|
||||||
|
*/
|
||||||
|
function ext_convert(string $tmp, string $orig): ?array {
|
||||||
|
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
|
||||||
|
if (!EXT_OFFICE || !in_array($ext, EXT_OFFICE_TYPES, true)) { return null; }
|
||||||
|
if (!is_executable(EXT_SOFFICE)) { return ['error' => 'can’t be converted: LibreOffice isn’t installed on the server.']; }
|
||||||
|
|
||||||
|
@set_time_limit(EXT_TIMEOUT * 2);
|
||||||
|
$tmpdir = sys_get_temp_dir();
|
||||||
|
$profile = $tmpdir . '/print-lo-profile';
|
||||||
|
$work = $tmpdir . '/print-lo-' . bin2hex(random_bytes(6));
|
||||||
|
$lock = fopen($tmpdir . '/print-lo.lock', 'c');
|
||||||
|
if ($lock) { flock($lock, LOCK_EX); } // LibreOffice allows one run per profile at a time
|
||||||
|
|
||||||
|
$result = ['error' => 'could not be converted to PDF. Try saving it as a PDF first.'];
|
||||||
|
if (@mkdir($work, 0700, true) && copy($tmp, $work . '/document.' . $ext)) {
|
||||||
|
$cmd = ['/usr/bin/timeout', '-k', '5', (string)EXT_TIMEOUT, EXT_SOFFICE,
|
||||||
|
'-env:UserInstallation=file://' . $profile, '--headless', '--norestore',
|
||||||
|
'--convert-to', 'pdf', '--outdir', $work, $work . '/document.' . $ext];
|
||||||
|
$p = @proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $work,
|
||||||
|
['PATH' => '/usr/bin:/bin', 'HOME' => $work, 'LC_ALL' => 'C.UTF-8']);
|
||||||
|
if (is_resource($p)) {
|
||||||
|
stream_get_contents($pipes[1]); stream_get_contents($pipes[2]);
|
||||||
|
fclose($pipes[1]); fclose($pipes[2]); proc_close($p);
|
||||||
|
}
|
||||||
|
$pdf = $work . '/document.pdf';
|
||||||
|
$dest = tempnam($tmpdir, 'prt');
|
||||||
|
if (is_file($pdf) && filesize($pdf) > 0 && $dest !== false && copy($pdf, $dest)) {
|
||||||
|
$result = ['path' => $dest, 'name' => cut(pathinfo($orig, PATHINFO_FILENAME), 80) . '.pdf'];
|
||||||
|
} else {
|
||||||
|
ext_rmdir($profile); // a crashed run can leave a broken profile; start fresh next time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ext_rmdir($work);
|
||||||
|
if ($lock) { flock($lock, LOCK_UN); fclose($lock); }
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ext_css(): string {
|
||||||
|
return '[hidden]{display:none!important}'
|
||||||
|
. '@media (max-width:480px){.file{flex-wrap:wrap}.fn{flex-basis:100%}}'
|
||||||
|
. '#pv{position:fixed;inset:0;z-index:50;display:flex;flex-direction:column;background:var(--bg)}'
|
||||||
|
. '.pvbar{display:flex;align-items:center;gap:8px;padding:max(10px,env(safe-area-inset-top)) max(12px,env(safe-area-inset-right)) 10px max(12px,env(safe-area-inset-left));background:var(--card);border-bottom:1px solid var(--line)}'
|
||||||
|
. '.pvbar b{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}'
|
||||||
|
. '.pvbar span{color:var(--mute);font-size:.88rem;white-space:nowrap}'
|
||||||
|
. '#pvbody{flex:1;overflow:auto;padding:12px;text-align:center;-webkit-overflow-scrolling:touch}'
|
||||||
|
. '#pvbody canvas,#pvbody img{display:block;height:auto;max-width:100%;margin:0 auto 12px;background:#fff;border:1px solid var(--line)}'
|
||||||
|
. '#pvbody pre{margin:0 auto;max-width:90ch;text-align:left;white-space:pre-wrap;overflow-wrap:anywhere;font:.9rem/1.45 ui-monospace,monospace;color:var(--ink)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Preview overlay + small scripts (remembers each device's option choices). */
|
||||||
|
function ext_page_html(): string {
|
||||||
|
$preview = !EXT_PREVIEW ? '' : <<<'HTML'
|
||||||
|
<div id="pv" hidden role="dialog" aria-modal="true" aria-label="Preview">
|
||||||
|
<div class="pvbar"><b id="pvname"></b><span id="pvinfo"></span>
|
||||||
|
<button type="submit" class="mini p" id="pvprint" form="f" name="reprint" value="" hidden>Print</button>
|
||||||
|
<button type="button" class="mini" id="pvclose">Close</button></div>
|
||||||
|
<div id="pvbody"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var pv=document.getElementById('pv'), body=document.getElementById('pvbody'), nm=document.getElementById('pvname'),
|
||||||
|
info=document.getElementById('pvinfo'), pp=document.getElementById('pvprint'), MAXP=40, lib=null, blobUrl=null, token=0;
|
||||||
|
function abs(q){return new URL(q,location.href).href;}
|
||||||
|
function kind(n){var e=(n.split('.').pop()||'').toLowerCase();return e==='pdf'?'pdf':e==='txt'?'txt':/^(jpe?g|png|gif)$/.test(e)?'img':'';}
|
||||||
|
function pdfjs(){if(!lib){lib=import(abs('?asset=pdf.min.mjs')).then(function(m){m.GlobalWorkerOptions.workerSrc=abs('?asset=pdf.worker.min.mjs');return m;});}return lib;}
|
||||||
|
function close(){token++;pv.hidden=true;body.innerHTML='';document.body.style.overflow='';if(blobUrl){URL.revokeObjectURL(blobUrl);blobUrl=null;}}
|
||||||
|
function fail(t,url,saved){
|
||||||
|
if(t!==token)return; lib=null; body.innerHTML='<p>Preview isn’t available for this file.</p>';
|
||||||
|
if(saved){var a=document.createElement('a');a.className='mini';a.href=url;a.target='_blank';a.rel='noopener';a.textContent='Open in a new tab';body.appendChild(a);}
|
||||||
|
}
|
||||||
|
function show(name,url,saved){
|
||||||
|
var k=kind(name),t=++token;
|
||||||
|
nm.textContent=name;info.textContent='';body.innerHTML='';pp.value=saved||'';pp.hidden=!saved;
|
||||||
|
pv.hidden=false;document.body.style.overflow='hidden';
|
||||||
|
if(k==='img'){var im=new Image();im.alt=name;im.src=url;body.appendChild(im);return;}
|
||||||
|
if(k==='txt'){fetch(url).then(function(r){return r.text();}).then(function(s){if(t!==token)return;var pre=document.createElement('pre');pre.textContent=s.slice(0,200000);body.appendChild(pre);}).catch(function(){fail(t,url,saved);});return;}
|
||||||
|
pdfjs().then(function(m){return m.getDocument({url:url}).promise;}).then(async function(doc){
|
||||||
|
var n=Math.min(doc.numPages,MAXP);
|
||||||
|
info.textContent=doc.numPages+(doc.numPages===1?' page':' pages')+(doc.numPages>n?' (showing first '+n+')':'');
|
||||||
|
for(var i=1;i<=n;i++){
|
||||||
|
var pg=await doc.getPage(i); if(t!==token)return;
|
||||||
|
var d=Math.min(window.devicePixelRatio||1,2),w=Math.min(body.clientWidth-24,900),b=pg.getViewport({scale:1}),
|
||||||
|
vp=pg.getViewport({scale:(w/b.width)*d}),c=document.createElement('canvas');
|
||||||
|
c.width=vp.width;c.height=vp.height;c.style.width=(vp.width/d)+'px';body.appendChild(c);
|
||||||
|
await pg.render({canvasContext:c.getContext('2d'),viewport:vp}).promise;
|
||||||
|
}
|
||||||
|
}).catch(function(){fail(t,url,saved);});
|
||||||
|
}
|
||||||
|
document.addEventListener('click',function(e){
|
||||||
|
var a=e.target.closest&&e.target.closest('[data-preview]'); if(!a)return;
|
||||||
|
e.preventDefault(); var n=a.getAttribute('data-preview'); show(n,abs('?view='+encodeURIComponent(n)),n);
|
||||||
|
});
|
||||||
|
document.getElementById('pvclose').addEventListener('click',close);
|
||||||
|
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&!pv.hidden)close();});
|
||||||
|
var inp=document.getElementById('files'),list=document.getElementById('picked');
|
||||||
|
if(inp&&list)inp.addEventListener('change',function(){
|
||||||
|
Array.prototype.forEach.call(inp.files,function(file,i){
|
||||||
|
var li=list.children[i]; if(!li||!kind(file.name))return;
|
||||||
|
var b=document.createElement('button');b.type='button';b.className='mini';b.style.marginLeft='8px';b.textContent='Preview';
|
||||||
|
b.addEventListener('click',function(){if(blobUrl)URL.revokeObjectURL(blobUrl);blobUrl=URL.createObjectURL(file);show(file.name,blobUrl,'');});
|
||||||
|
li.appendChild(b);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
HTML;
|
||||||
|
|
||||||
|
$remember = <<<'HTML'
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var F=document.getElementById('f'); if(!F)return;
|
||||||
|
try{
|
||||||
|
['duplex','mono'].forEach(function(n){var el=F.elements[n]; if(!el)return;
|
||||||
|
var v=localStorage.getItem('lanprint:'+n); if(v!==null)el.checked=(v==='1');
|
||||||
|
el.addEventListener('change',function(){try{localStorage.setItem('lanprint:'+n,el.checked?'1':'0');}catch(e){}});});
|
||||||
|
['nup','printer'].forEach(function(n){var el=F.elements[n]; if(!el)return;
|
||||||
|
var v=localStorage.getItem('lanprint:'+n);
|
||||||
|
if(v!==null)Array.prototype.forEach.call(el.options,function(o){if(o.value===v)el.value=v;});
|
||||||
|
el.addEventListener('change',function(){try{localStorage.setItem('lanprint:'+n,el.value);}catch(e){}});});
|
||||||
|
}catch(e){}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
HTML;
|
||||||
|
return $preview . "\n" . $remember;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Internals ----------
|
||||||
|
|
||||||
|
function ext_serve(string $name): void {
|
||||||
|
$path = stored_path($name);
|
||||||
|
$ext = $path === null ? '' : strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||||
|
if ($path === null || !isset(ALLOWED[$ext])) { http_response_code(404); exit('Not found'); }
|
||||||
|
header_remove('Content-Security-Policy');
|
||||||
|
header('Content-Type: ' . ALLOWED[$ext][0] . ($ext === 'txt' ? '; charset=utf-8' : ''));
|
||||||
|
header('Content-Length: ' . (string)filesize($path));
|
||||||
|
header("Content-Disposition: inline; filename*=UTF-8''" . rawurlencode(basename($path)));
|
||||||
|
readfile($path);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ext_asset(string $name): void {
|
||||||
|
$file = EXT_VENDOR . '/' . $name;
|
||||||
|
if (!in_array($name, ['pdf.min.mjs', 'pdf.worker.min.mjs'], true) || !is_file($file)) { http_response_code(404); exit('Not found'); }
|
||||||
|
header_remove('Content-Security-Policy');
|
||||||
|
header('Content-Type: text/javascript; charset=utf-8');
|
||||||
|
header('Cache-Control: public, max-age=86400');
|
||||||
|
header('Content-Length: ' . (string)filesize($file));
|
||||||
|
readfile($file);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ext_rmdir(string $dir): void {
|
||||||
|
if (!is_dir($dir)) { return; }
|
||||||
|
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
|
||||||
|
foreach ($it as $f) { ($f->isDir() && !$f->isLink()) ? @rmdir($f->getPathname()) : @unlink($f->getPathname()); }
|
||||||
|
@rmdir($dir);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue