Cache handling for thumbnails thanks to ajax

master
Y 2016-01-30 22:43:41 +01:00 committed by tYYGH
parent 1f47656be8
commit a56bda94bd
9 changed files with 416 additions and 81 deletions

10
README
View File

@ -31,9 +31,11 @@ Finally, all the input (search terms, etc.) is verified by the CLI script, so th
The shell script may be copied anywhere. Its dependencies are standard, although I only tested on Debian and Arch Linux: bash, find, gawk, sed, grep, file, base64, ls. Besides, if pdfinfo and pdftoppm are installed, the script detects these and handles the pages from PDF documents in an individual manner (as JPEG images), instead of returning the whole document when a single page is requested.
Once the script is installed, the path to the Paperwork data must be changed (edit the script). Optionally, the number of DPI (dots per inch) for the extraction of pages from PDF documents may be adjusted as well.
Same goes for the PHP file's installation (```index.php```). It depends on PHP 5.2 or better (so that JSON is included).
Alongside the PHP file, there must be a configuration file named ```paperweb.ini.php```, which should be created by copying and changing the provided example file: ```example-paperweb.ini.php```.
The CSS file is optional, and it can be adapted.
On the web side, the only mandatory files are ```index.php```, ```query.inc.php```, ```thumbs.inc.php```, and ```paperweb.ini.php```, which should be created by copying and changing the provided example file: ```example-paperweb.ini.php```. The code depends on PHP 5.2 or better (so that JSON is included).
Other, optional, files are provided in order to offer better cache handling, by using HTTP GET requests through Ajax calls, instead of relying on the POST requests that are necessary for Javascript-less plain HTML. These files are:
* ```ajax.js```: a very minimal Ajax helper;
* ```query.ajax.php``` and ```thumbs.ajax.php```: the receiving components of Ajax calls.
And theres the CSS file, which is optional too, although it is much better to use it, even though it could be improved.
Finally, ```visudo``` must be run so that the web server user is allowed to execute the CLI script, for example:
```
@ -116,4 +118,4 @@ $ paperfind.sh -D 20151217_0000_01 -p 3 | json_pp
= THE WEB INTERFACE =
There is not much to say, appart from the fact that the web server should probably be configured to authenticate users before granting access to the interface. It depends on the value of the data managed by Paperwork.
The current web UI is minimal and very much a prototype. I may improve it, depending on my own needs, or if someone volunteers to contribute ;-)
The current web UI is minimal and barely more than a prototype. I may improve it, depending on my own needs, or if someone volunteers to contribute ;-)

197
web/ajax.js Normal file
View File

@ -0,0 +1,197 @@
// PAPERWEB - GPLv3 licence
// Copyright 2006-2016 Yves Gablin
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// = Credits =
// Tutorial: http://robloche.free.fr/javascript/tuto_xhr/tuto_xhr.html
var ajax = {
XML: 1,
TXT: 2,
// public class Connection
// + start()
// - xmlhttp
Connection: function() {
var xh;
try {
xh = new XMLHttpRequest();
} catch (e1) { // IE
try {
xh = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e2) {
try {
xh = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e3) {
xh = null;
}
}
}
this.xmlhttp = xh;
},
// private class Link
// - connection: Connection
// - urlBuilder: user-provided function
// - resultHandler: user-provided function
// - resultType: XML|TXT
// - preventDefault : true|false = prevent default event consequence
Link: function(pSource, pEvent, pUrlBuilder, pResultHandler, pType, pPreventDefault) {
this.connection = new ajax.Connection();
this.urlBuilder = pUrlBuilder;
this.resultHandler = pResultHandler;
this.resultType = pType;
this.preventDefault = pPreventDefault;
if (!pSource.ajaxLinks) {
pSource.ajaxLinks = new Array();
pSource.addEventListener(pEvent, this.evhSourceTrigger);
}
pSource.ajaxLinks[pSource.ajaxLinks.length] = this;
if (pEvent != 'click') {
this.evhSourceTrigger.call(pSource, null);
}
},
// public static function link()
link: function(pSource, pUrlBuilder, pDestHandler, pType, pPreventDefault) {
var event;
switch (pSource.tagName) {
case 'SELECT':
case 'TEXTAREA':
event = 'change';
break;
case 'BUTTON':
event = 'click';
break;
case 'INPUT':
switch (pSource.type) {
case 'text':
case 'password':
case 'file':
case 'hidden':
event = 'change';
break;
case 'checkbox':
case 'radio':
case 'submit':
case 'reset':
case 'image':
case 'button':
event = 'click';
break;
}
break;
}
new ajax.Link(pSource, event, pUrlBuilder, pDestHandler, pType, pPreventDefault);
},
// public static function importNode()
importNode: function(pSourceNode, pDestDoc) {
var destNode;
switch (pSourceNode.nodeType) {
case 1: // Element
destNode = pDestDoc.createElement(pSourceNode.tagName);
break;
case 3: // TextNode
destNode = pDestDoc.createTextNode(pSourceNode.data);
break;
case 4: // CDATASection
destNode = pDestDoc.createCDATASection(pSourceNode.data);
break;
case 5: // EntityReference
destNode = pDestDoc.createEntityReference(pSourceNode.nodeName);
break;
case 7: // ProcessingInstruction
destNode = pDestDoc.createProcessingInstruction(pSourceNode.nodeName, pSourceNode.nodeValue);
break;
case 8: // Comment
destNode = pDestDoc.createComment(pSourceNode.data);
break;
case 9: // Document
return ajax.importNode(pSourceNode.documentElement, pDestDoc);
default:
return null;
}
if (pSourceNode.attributes) {
var att;
for (var numAtt = 0; numAtt < pSourceNode.attributes.length; numAtt++) {
att = pSourceNode.attributes[numAtt];
destNode.setAttribute(att.name, att.value);
}
}
if (pSourceNode.childNodes) {
var child;
for (var numCh = 0; numCh < pSourceNode.childNodes.length; numCh++) {
child = ajax.importNode(pSourceNode.childNodes[numCh], pDestDoc);
if (child != null) destNode.appendChild(child);
}
}
return destNode;
}
}
// public method Connection.start()
ajax.Connection.prototype.start = function(pUrl, pPostParams, pCallback, pType) {
if (!this.xmlhttp || !pCallback) return;
var xh = this.xmlhttp;
var async = true;
if (xh.ajaxConnectionConnected == true) {
xh.abort();
}
if (pPostParams) {
xh.open("POST", pUrl, async);
} else {
xh.open("GET", pUrl, async);
}
xh.onreadystatechange = function() {
if (xh.readyState == 4) {
var status = -1;
try { status = xh.status; } catch (unreachable) { }
if (xh.ajaxConnectionNotice) {
xh.ajaxConnectionNotice.parentNode.removeChild(xh.ajaxConnectionNotice);
xh.ajaxConnectionNotice = null;
}
var result = null;
if (status == 200) {
if (pType == ajax.XML) {
result = xh.responseXML;
} else {
result = xh.responseText;
}
}
xh.ajaxConnectionConnected = false;
pCallback(result);
}
};
var notice = document.createElement('div');
notice.appendChild(document.createTextNode('...'));
notice.className = 'ajax-connection-wait';
xh.ajaxConnectionNotice = document.getElementsByTagName('body').item(0).appendChild(notice);
if (pType == ajax.XML)
xh.overrideMimeType("text/xml");
xh.ajaxConnectionConnected = true;
xh.send(pPostParams);
};
// public method Link.evhSourceTrigger()
ajax.Link.prototype.evhSourceTrigger = function(pEvt) {
var link, prevent = false;
for (var i = 0; i < this.ajaxLinks.length; i++) {
link = this.ajaxLinks[i];
link.connection.start(link.urlBuilder(this), null, link.resultHandler, link.resultType);
prevent = prevent || link.preventDefault;
}
if (prevent) {
pEvt.preventDefault();
}
};

View File

@ -31,6 +31,7 @@ if (array_key_exists('doDownload', $_REQUEST)) {
$ext = explode('/', $meta['mime'])[1];
header("Content-Type: {$meta['mime']}");
header("ETag: \"{$meta['etag']}\"");
header("Cache-Control: public");
header("Content-Disposition: inline; filename=\"{$date}_{$page}.{$ext}\"");
passthru("sudo -u {$CONF['USER']} {$CONF['PATH']} -R {$datearg} -p {$pagearg}");
}
@ -41,6 +42,8 @@ if (array_key_exists('doDownload', $_REQUEST)) {
<html lang="en"><head>
<meta charset="utf-8">
<title>PaperWeb, a simple web interface for Paperwork</title>
<script type="text/javascript" src="ajax.js"></script>
<script type="text/javascript" src="paperweb.js"></script>
<link rel="stylesheet" href="paperweb.css"/>
</head><body>
<form method="POST">
@ -54,85 +57,10 @@ if (array_key_exists('doDownload', $_REQUEST)) {
<input name="K" value="<?php echo htmlentities(@$_REQUEST['K']); ?>"/></label></p>
<p><button type="submit" name="doQuery" value="1">Refresh</button></p>
</header>
<section id="result">
<?php
if (array_key_exists('doQuery', $_REQUEST)) {
$pattern='/(?<=")(?:[^"]|\\\\")*[^" ](?:[^"]|\\\\")*(?=")|[^ "]+/';
preg_match_all($pattern, $_REQUEST['D'], $matches);
$dates = escapeshellarg(implode('|', $matches[0]));
preg_match_all($pattern, $_REQUEST['L'], $matches);
$labels = escapeshellarg(implode('|', $matches[0]));
preg_match_all($pattern, $_REQUEST['K'], $matches);
$words = escapeshellarg(implode('|', $matches[0]));
$json = exec("sudo -u {$CONF['USER']} {$CONF['PATH']} -Q -d {$dates} -l {$labels} -k {$words} -i");
unset($_REQUEST['doThumbnails']);
unset($_REQUEST['thumbnailsDone']);
unset($_REQUEST['currentDoc']);
} else {
$json = @$_REQUEST['queryDone'];
}
if ($json) {
if ($json == '[]') {
require('query.inc.php');
require('thumbs.inc.php');
?>
<p>No result.</p>
<?php
} else {
$current = (array_key_exists('doThumbnails', $_REQUEST) ? $_REQUEST['doThumbnails'] : @$_REQUEST['currentDoc']);
?>
<h2>Documents found</h2>
<input type="hidden" name="queryDone" value="<?php echo htmlentities($json); ?>"/>
<?php
foreach (json_decode($json, true) as $doc) {
?>
<button type="submit" name="doThumbnails" value="<?php echo htmlentities($doc['folder']); ?>"<?php
if ($doc['folder'] == $current) {
echo ' disabled="disabled"';
}
?>>
<h3><?php echo preg_replace('/^(....)(..)(..).*$/', '$1-$2-$3', $doc['folder']); ?></h3>
<p><?php echo $doc['count']; ?> pages (<?php echo ($doc['type']=='pdf'?'PDF':'images'); ?>)</p>
<?php if (count($doc['labels'])) { ?>
<ul>
<?php foreach ($doc['labels'] as $lab) { ?>
<li><?php echo htmlentities($lab); ?></li>
<?php } ?>
</ul>
<?php } ?>
</button>
<?php
}
}
}
?>
</section>
<section id="thumbs">
<?php
if (array_key_exists('doThumbnails', $_REQUEST)) {
$date = $_REQUEST['doThumbnails'];
$datearg = escapeshellarg($date);
$json = exec("sudo -u {$CONF['USER']} {$CONF['PATH']} -T {$datearg}");
} else {
$json = @$_REQUEST['thumbnailsDone'];
$date = @$_REQUEST['currentDoc'];
}
if ($json) {
?>
<h2>Pages</h2>
<input type="hidden" name="thumbnailsDone" value="<?php echo htmlentities($json); ?>"/>
<input type="hidden" name="currentDoc" value="<?php echo htmlentities($date); ?>"/>
<?php
foreach (json_decode($json, true) as $n => $p) {
$nump = $n+1;
?>
<a target="_blank" href="?<?php echo htmlentities("doDownload=1&date={$date}&page={$nump}"); ?>"><img
src="data:<?php echo $p['mime']; ?>;base64,<?php echo $p['data']; ?>"
width="<?php echo $p['width']; ?>" height="<?php echo $p['height']; ?>"
alt="Page <?php echo $nump; ?>"/></a>
<?php
}
}
?>
</section>
</form>
<footer><p>PaperWeb - Copyright 2016 Yves Gablin - Based on Paperwork by jflesch.</p></footer>
</body></html>

View File

@ -47,3 +47,12 @@ footer {
font-size: 90%;
font-style: italic;
}
.ajax-connection-wait {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
text-align: center;
font-size: 10em;
}

73
web/paperweb.js Normal file
View File

@ -0,0 +1,73 @@
// PAPERWEB - GPLv3 licence
// Copyright 2006-2016 Yves Gablin
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
var paperweb = {
//private static function purgeSection()
purgeSection: function(section) {
for (var i = section.childNodes.length; i--;) {
section.removeChild(section.childNodes[i]);
}
},
// public static function prepareQuery()
prepareQuery: function() {
var button = document.querySelector('header button');
ajax.link(button,
function(thisButton) {
var inputs = document.querySelectorAll('header input');
return encodeURI('query.ajax.php?doQuery=1&D='+inputs[0].value
+'&L='+inputs[1].value+'&K='+inputs[2].value);
},
paperweb.updateResult,
ajax.XML, true);
},
// public static function prepareThumbs()
prepareThumbs: function() {
var buttons = document.querySelectorAll('#result button');
for (var i = buttons.length; i--;) {
buttons[i].addEventListener('click', paperweb.changeCurrentDocument);
ajax.link(buttons[i],
function(thisButton) {
return encodeURI('thumbs.ajax.php?doThumbnails='+thisButton.value);
},
paperweb.updateThumbs,
ajax.XML, true);
}
},
// public static function updateResult(xml)
updateResult: function(xml) {
var newSection = ajax.importNode(xml.documentElement, document);
var section = document.getElementById('result');
section.parentNode.replaceChild(newSection, section);
paperweb.purgeSection(document.getElementById('thumbs'));
paperweb.prepareThumbs();
},
// public static function updateThumbs(xml)
updateThumbs: function(xml) {
var section = document.getElementById('thumbs');
section.parentNode.replaceChild(ajax.importNode(xml.documentElement, document), section);
},
// public static function changeCurrentDocument(evt)
changeCurrentDocument: function(evt) {
var buttons = document.querySelectorAll('#result button');
for (var i = buttons.length; i--;) {
buttons[i].disabled = (buttons[i] === this);
}
}
};
window.addEventListener('load', paperweb.prepareQuery);

17
web/query.ajax.php Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<?php
# PAPERWEB - GPLv3 licence
# Copyright 2016 Yves Gablin
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
$CONF = parse_ini_file('paperweb.ini.php');
require('query.inc.php');

51
web/query.inc.php Normal file
View File

@ -0,0 +1,51 @@
<section id="result">
<?php
if (array_key_exists('doQuery', $_REQUEST)) {
$pattern='/(?<=")(?:[^"]|\\\\")*[^" ](?:[^"]|\\\\")*(?=")|[^ "]+/';
preg_match_all($pattern, $_REQUEST['D'], $matches);
$dates = escapeshellarg(implode('|', $matches[0]));
preg_match_all($pattern, $_REQUEST['L'], $matches);
$labels = escapeshellarg(implode('|', $matches[0]));
preg_match_all($pattern, $_REQUEST['K'], $matches);
$words = escapeshellarg(implode('|', $matches[0]));
$json = exec("sudo -u {$CONF['USER']} {$CONF['PATH']} -Q -d {$dates} -l {$labels} -k {$words} -i");
unset($_REQUEST['doThumbnails']);
unset($_REQUEST['thumbnailsDone']);
unset($_REQUEST['currentDoc']);
} else {
$json = @$_REQUEST['queryDone'];
}
if ($json) {
if ($json == '[]') {
?>
<p>No result.</p>
<?php
} else {
$current = (array_key_exists('doThumbnails', $_REQUEST) ? $_REQUEST['doThumbnails'] : @$_REQUEST['currentDoc']);
?>
<h2>Documents found</h2>
<input type="hidden" name="queryDone" value="<?php echo htmlentities($json, ENT_COMPAT|ENT_XML1); ?>"/>
<?php
foreach (json_decode($json, true) as $doc) {
?>
<button type="submit" name="doThumbnails" value="<?php echo htmlentities($doc['folder'], ENT_COMPAT|ENT_XML1); ?>"<?php
if ($doc['folder'] == $current) {
echo ' disabled="disabled"';
}
?>>
<h3><?php echo preg_replace('/^(....)(..)(..).*$/', '$1-$2-$3', $doc['folder']); ?></h3>
<p><?php echo $doc['count']; ?> pages (<?php echo ($doc['type']=='pdf'?'PDF':'images'); ?>)</p>
<?php if (count($doc['labels'])) { ?>
<ul>
<?php foreach ($doc['labels'] as $lab) { ?>
<li><?php echo htmlentities($lab, ENT_COMPAT|ENT_XML1); ?></li>
<?php } ?>
</ul>
<?php } ?>
</button>
<?php
}
}
}
?>
</section>

30
web/thumbs.ajax.php Normal file
View File

@ -0,0 +1,30 @@
<?php
# PAPERWEB - GPLv3 licence
# Copyright 2016 Yves Gablin
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
$CONF = parse_ini_file('paperweb.ini.php');
$datearg = escapeshellarg($_REQUEST['doThumbnails']);
$json = exec("sudo -u {$CONF['USER']} {$CONF['PATH']} -M {$datearg}");
$meta = json_decode($json, true);
$knownetag = trim(@$_SERVER['HTTP_IF_NONE_MATCH'], ' "');
if ($meta['etag'] == $knownetag) {
header('HTTP/1.1 304 Not Modified');
} else {
header("ETag: \"{$meta['etag']}\"");
header("Cache-Control: public");
?>
<?xml version="1.0" encoding="UTF-8"?>
<?php
require('thumbs.inc.php');
}

28
web/thumbs.inc.php Normal file
View File

@ -0,0 +1,28 @@
<section id="thumbs">
<?php
if (array_key_exists('doThumbnails', $_REQUEST)) {
$date = $_REQUEST['doThumbnails'];
$datearg = escapeshellarg($date);
$json = exec("sudo -u {$CONF['USER']} {$CONF['PATH']} -T {$datearg}");
} else {
$json = @$_REQUEST['thumbnailsDone'];
$date = @$_REQUEST['currentDoc'];
}
if ($json) {
?>
<h2>Pages</h2>
<input type="hidden" name="thumbnailsDone" value="<?php echo htmlentities($json, ENT_COMPAT|ENT_XML1); ?>"/>
<input type="hidden" name="currentDoc" value="<?php echo htmlentities($date, ENT_COMPAT|ENT_XML1); ?>"/>
<?php
foreach (json_decode($json, true) as $n => $p) {
$nump = $n+1;
?>
<a target="_blank" href="<?php echo htmlentities("?doDownload=1&date={$date}&page={$nump}", ENT_COMPAT|ENT_XML1); ?>"><img
src="data:<?php echo $p['mime']; ?>;base64,<?php echo $p['data']; ?>"
width="<?php echo $p['width']; ?>" height="<?php echo $p['height']; ?>"
alt="Page <?php echo $nump; ?>"/></a>
<?php
}
}
?>
</section>