From 77db160d54bc81dc28ebf229ae7bf7e45addfd0e Mon Sep 17 00:00:00 2001 From: Uwe Steinmann Date: Sat, 10 Dec 2022 11:25:22 +0100 Subject: [PATCH] first commit --- README.md | 18 + changelog.md | 4 + class.paperless.php | 928 ++++++++++++++++++++++++++++++++++++++++++++ conf.php | 46 +++ icon.svg | 1 + lang.php | 12 + 6 files changed, 1009 insertions(+) create mode 100644 README.md create mode 100644 changelog.md create mode 100644 class.paperless.php create mode 100644 conf.php create mode 100644 icon.svg create mode 100644 lang.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b2d0f2 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +SeedDMS paperless extension +============================ + +Paperless (and Paperless ngx) is another free document management system. +It has a different focus than SeedDMS and misses many of the features +of SeedDMS but there three Android apps for uploading and browsing. + +All are available at google and/or f-droid. + +paperless-mobile + This is the youngest but most feature complete app + +paperless + This one is rather old and development and + +paperless-share + This app is just to add a share button. Any shared document will + be uploaded to the server. diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..25de560 --- /dev/null +++ b/changelog.md @@ -0,0 +1,4 @@ +Changes in version 1.0.0 +========================== + +- Initial version diff --git a/class.paperless.php b/class.paperless.php new file mode 100644 index 0000000..e56a99d --- /dev/null +++ b/class.paperless.php @@ -0,0 +1,928 @@ + +* All rights reserved +* +* This script is part of the SeedDMS project. The SeedDMS project 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 2 of the License, or +* (at your option) any later version. +* +* The GNU General Public License can be found at +* http://www.gnu.org/copyleft/gpl.html. +* +* This script 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. +* +* This copyright notice MUST APPEAR in all copies of the script! +***************************************************************/ + + +/** + * Paperless extension + * + * @author Uwe Steinmann + * @package SeedDMS + * @subpackage example + */ +class SeedDMS_ExtPaperless extends SeedDMS_ExtBase { /* {{{ */ + + /** + * Initialization + * + * Use this method to do some initialization like setting up the hooks + * You have access to the following global variables: + * $GLOBALS['settings'] : current global configuration + * $GLOBALS['settings']->_extensions['example'] : configuration of this extension + * $GLOBALS['LANG'] : the language array with translations for all languages + * $GLOBALS['SEEDDMS_HOOKS'] : all hooks added so far + */ + function init() { /* {{{ */ + $GLOBALS['SEEDDMS_HOOKS']['initRestAPI'][] = new SeedDMS_ExtPaperless_RestAPI; + } /* }}} */ + + function main() { /* {{{ */ + } /* }}} */ +} /* }}} */ + +use Psr\Container\ContainerInterface; + +class SeedDMS_ExtPaperless_RestAPI_Controller { /* {{{ */ + protected $container; + + protected function __getDocumentData($document) { /* {{{ */ + $fulltextservice = $this->container->fulltextservice; + + $content = ''; + $index = $fulltextservice->Indexer(); + if($index) { + $lucenesearch = $fulltextservice->Search(); + if($searchhit = $lucenesearch->getDocument($document->getID())) { + $idoc = $searchhit->getDocument(); + try { + $content = htmlspecialchars(mb_strimwidth($idoc->getFieldValue('content'), 0, 1000, '...')); + } catch (Exception $e) { + } + } + } + + $lc = $document->getLatestContent(); + $cats = $document->getCategories(); + $tags = array(); + foreach($cats as $cat) + $tags[] = (int) $cat->getId(); + $data = array( + 'id'=>(int)$document->getId(), + 'correspondent'=>null, + 'document_type'=>null, + 'storage_path'=>null, + 'title'=>$document->getName(), + 'content'=>$content, + 'tags'=>$tags, + 'checksum'=>$lc->getChecksum(), + 'created'=>date('Y-m-d\TH:i:s+02:00', $document->getDate()), + 'created_date'=>date('Y-m-d', $document->getDate()), + 'modified'=>date('Y-m-d\TH:i:s+02:00', $document->getDate()), + 'added'=>date('Y-m-d\TH:i:s+02:00', $document->getDate()), + 'archive_serial_number'=>null, + 'original_file_name'=>$lc->getOriginalFileName(), + 'archived_file_name'=>$lc->getOriginalFileName() + ); + return $data; + } /* }}} */ + + public function getContrastColor($hexcolor) { + $r = hexdec(substr($hexcolor, 1, 2)); + $g = hexdec(substr($hexcolor, 3, 2)); + $b = hexdec(substr($hexcolor, 5, 2)); + $yiq = (($r * 299) + ($g * 587) + ($b * 114)) / 1000; + return ($yiq >= 128) ? '000000' : 'ffffff'; + } + + protected function __getCategoryData($category) { /* {{{ */ + $color = substr(md5($category->getName()), 0, 6); + $data = [ + 'id'=>(int)$category->getId(), + 'slug'=>strtolower($category->getName()), + 'name'=>$category->getName(), + 'colour'=>'#'.$color, //'#50b02c', + 'text_color'=>'#'.$this->getContrastColor($color), + 'match'=>'', + 'matching_algorithm'=>6, + 'is_insensitive'=>true, + 'is_inbox_tag'=>$category->getName() == 'Computer', + 'document_count'=>0 + ]; + return $data; + } /* }}} */ + + // constructor receives container instance + public function __construct(ContainerInterface $container) { /* {{{ */ + $this->container = $container; + } /* }}} */ + + function api($request, $response) { /* {{{ */ + $data = array( + 'correspondents'=>$request->getUri().'correspondents/', + 'document_types'=>$request->getUri().'document_types/', + 'documents'=>$request->getUri().'documents/', + 'logs'=>$request->getUri().'logs/', + 'tags'=>$request->getUri().'tags/', + 'saved_views'=>$request->getUri().'saved_views/', + 'storage_paths'=>$request->getUri().'storage_paths/', + 'tasks'=>$request->getUri().'tasks/', + ); + + return $response->withJson($data, 200); + } /* }}} */ + + function token($request, $response) { /* {{{ */ + $settings = $this->container->config; + $authenticator = $this->container->authenticator; + $logger = $this->container->logger; + + $logger->log(var_export($request->getParsedBody(), true), PEAR_LOG_INFO); + $data = $request->getParsedBody(); + if(empty($data['username'])) { + $body = $request->getBody(); + $data = json_decode($body, true); + } + if($data) { + $userobj = $authenticator->authenticate($data['username'], $data['password']); + if(!$userobj) + return $response->withJson(array('token'=>''), 403); + else { + if(!empty($settings->_extensions['paperless']['jwtsecret'])) { + $token = new SeedDMS_JwtToken($settings->_extensions['paperless']['jwtsecret']); + if(!$tokenstr = $token->jwtEncode($userobj->getId().':'.(time()+84600))) { + return $response->withStatus(403); + } + return $response->withJson(array('token'=>$tokenstr), 200); + } else { + return $response->withJson(array('token'=>$settings->_apiKey), 200); + } + } + } + return $response->withStatus(403); + } /* }}} */ + + function tags($request, $response) { /* {{{ */ + $dms = $this->container->dms; + $userobj = $this->container->userobj; + $settings = $this->container->config; + $fulltextservice = $this->container->fulltextservice; + $logger = $this->container->logger; + + if(false === ($categories = $dms->getDocumentCategories())) { + return $response->withJson(array('results'=>null), 500); + } + + if(!empty($settings->_extensions['paperless']['usehomefolder'])) { + if(!($startfolder = $userobj->getHomeFolder())) + $startfolder = $dms->getFolder($settings->_rootFolderID); + } elseif(!isset($settings->_extensions['paperless']['rootfolder']) || !($startfolder = $dms->getFolder($settings->_extensions['paperless']['rootfolder']))) + $startfolder = $dms->getFolder($settings->_rootFolderID); + + $index = $fulltextservice->Indexer(); + if($index) { + $lucenesearch = $fulltextservice->Search(); + $searchresult = $lucenesearch->search('', array('record_type'=>['document'], 'user'=>[$userobj->getLogin()], 'startFolder'=>$startfolder, 'rootFolder'=>$startfolder), array('limit'=>20), $order); + if($searchresult === false) { + return $response->withStatus(500); + } else { + $recs = array(); + $facets = $searchresult['facets']; + $logger->log(var_export($facets, true), PEAR_LOG_INFO); + } + } + + $data = []; + foreach($categories as $category) { + $tmp = $this->__getCategoryData($category); + if(isset($facets['category'][$category->getName()])) + $tmp['document_count'] = (int) $facets['category'][$category->getName()]; + $data[] = $tmp; + } + return $response->withJson(array('count'=>count($data), 'next'=>null, 'previous'=>null, 'results'=>$data), 200); + } /* }}} */ + + function correspondents($request, $response) { /* {{{ */ + //file_put_contents("php://stdout", var_dump($request, true)); + + $correspondents = array( + ); + return $response->withJson(array('count'=>count($correspondents), 'next'=>null, 'previous'=>null, 'results'=>$correspondents), 200); + } /* }}} */ + + function document_types($request, $response) { /* {{{ */ + //file_put_contents("php://stdout", var_dump($request, true)); + + $types = array( + ); + return $response->withJson(array('count'=>count($types), 'next'=>null, 'previous'=>null, 'results'=>$types), 200); + } /* }}} */ + + function saved_views($request, $response) { /* {{{ */ + //file_put_contents("php://stdout", var_dump($request, true)); + + $views = array( + ); + return $response->withJson(array('count'=>count($views), 'next'=>null, 'previous'=>null, 'results'=>$views), 200); + } /* }}} */ + + function storage_paths($request, $response) { /* {{{ */ + //file_put_contents("php://stdout", var_dump($request, true)); + + $paths = array( + ); + return $response->withJson(array('count'=>count($paths), 'next'=>null, 'previous'=>null, 'results'=>$paths), 200); + } /* }}} */ + + function documents($request, $response) { /* {{{ */ + $dms = $this->container->dms; + $userobj = $this->container->userobj; + $settings = $this->container->config; + $fulltextservice = $this->container->fulltextservice; + $logger = $this->container->logger; + + $params = $request->getQueryParams(); + $logger->log(var_export($params, true), PEAR_LOG_INFO); + + if(!empty($settings->_extensions['paperless']['usehomefolder'])) { + if(!($startfolder = $userobj->getHomeFolder())) + $startfolder = $dms->getFolder($settings->_rootFolderID); + } elseif(!isset($settings->_extensions['paperless']['rootfolder']) || !($startfolder = $dms->getFolder($settings->_extensions['paperless']['rootfolder']))) + $startfolder = $dms->getFolder($settings->_rootFolderID); + + $logger->log('Searching for documents in folder '.$startfolder->getId(), PEAR_LOG_DEBUG); + + $fullsearch = true; + if($fullsearch) { + if (isset($params["query"]) && is_string($params["query"])) { + $query = $params["query"]; + } + else { + $query = ""; + } + + $order = []; + if (isset($params["ordering"]) && is_string($params["ordering"])) { + if($params["ordering"][0] == '-') { + $order['dir'] = 'asc'; + $orderfield = substr($params["ordering"], 1); + } else { + $order['dir'] = 'desc'; + $orderfield = $params["ordering"]; + } + if(in_array($orderfield, ['created', 'title'])) + $order['by'] = $orderfield; + elseif($orderfield == 'added') + $order['by'] = 'created'; + } + + // category + $categories = array(); + $categorynames = array(); + if(isset($params['tags__id'])) { + $catid = (int) $params['tags__id']; + if($catid) { + if($cat = $dms->getDocumentCategory($catid)) { + $categories[] = $cat; + $categorynames[] = $cat->getName(); + } + } + } + /* tags__id__in is used when searching for documents by id */ + if(isset($params['tags__id__all'])) { + $catids = explode(',', $params['tags__id__all']); + foreach($catids as $catid) + if($catid) { + if($cat = $dms->getDocumentCategory($catid)) { + $categories[] = $cat; + $categorynames[] = $cat->getName(); + } + } + } + /* tags__id__in is used when getting the documents of the inbox */ + if(isset($params['tags__id__in'])) { + $catid = (int) $params['tags__id__in']; + if($catid) { + if($cat = $dms->getDocumentCategory($catid)) { + $categories[] = $cat; + $categorynames[] = $cat->getName(); + } + } + } + + $astart = 0; + if(isset($params['added__date__gt'])) { + $astart = (int) makeTsFromDate($params['added__date__gt']); + } + $aend = 0; + if(isset($params['added__date__lt'])) { + $aend = (int) makeTsFromDate($params['added__date__lt']); + } + + $index = $fulltextservice->Indexer(); + if($index) { + $lucenesearch = $fulltextservice->Search(); + $searchresult = $lucenesearch->search($query, array('record_type'=>['document'], 'user'=>[$userobj->getLogin()], 'category'=>$categorynames, 'created_start'=>$astart, 'created_end'=>$aend, 'startFolder'=>$startfolder, 'rootFolder'=>$startfolder), array('limit'=>20), $order); + if($searchresult === false) { + return $response->withStatus(500); + } else { + $recs = array(); + $facets = $searchresult['facets']; + $dcount = 0; + $fcount = 0; + if($searchresult) { + foreach($searchresult['hits'] as $hit) { + if($hit['document_id'][0] == 'D') { + if($tmp = $dms->getDocument(substr($hit['document_id'], 1))) { + // if($tmp->getAccessMode($user) >= M_READ) { + $tmp->verifyLastestContentExpriry(); + $recs[] = $this->__getDocumentData($tmp); + // } + } + } + } + } + } + } + } + + return $response->withJson(array('count'=>count($recs), 'next'=>null, 'previous'=>null, 'results'=>$recs), 200); + } /* }}} */ + + function autocomplete($request, $response) { /* {{{ */ + $dms = $this->container->dms; + $userobj = $this->container->userobj; + $settings = $this->container->config; + $fulltextservice = $this->container->fulltextservice; + $logger = $this->container->logger; + + $params = $request->getQueryParams(); + $query = $params['term']; + $logger->log(var_export($params, true), PEAR_LOG_INFO); + + $list = []; + $index = $fulltextservice->Indexer(); + if($index) { + if($terms = $index->terms($query, 'title')) { + foreach($terms as $term) + $list[] = $term->text; + } + } + + + return $response->withJson($list, 200); + } /* }}} */ + + function ui_settings($request, $response) { /* {{{ */ + $dms = $this->container->dms; + $userobj = $this->container->userobj; + $settings = $this->container->config; + $logger = $this->container->logger; + + $data = array( + 'user_id'=>$userobj->getId(), + 'username'=>$userobj->getLogin(), + 'display_name'=>$userobj->getFullName(), + 'settings'=>array('update_checking'=>array('backend_setting'=>'default')), + ); + return $response->withJson($data, 200); + } /* }}} */ + + function fetch_thumb($request, $response, $args) { /* {{{ */ + return $response->withRedirect($request->getUri()->getBasePath().'/api/documents/'.$args['id'].'/thumb/', 302); + } /* }}} */ + + function documents_thumb($request, $response, $args) { /* {{{ */ + require_once "SeedDMS/Preview.php"; + + $dms = $this->container->dms; + $userobj = $this->container->userobj; + $settings = $this->container->config; + $conversionmgr = $this->container->conversionmgr; + $logger = $this->container->logger; + + if (!isset($args['id']) || !$args['id']) + return $response->withStatus(404); + + $document = $dms->getDocument($args['id']); + if($document) { + if($document->getAccessMode($userobj) >= M_READ) { + $object = $document->getLatestContent(); + $width = 400; + $previewer = new SeedDMS_Preview_Previewer($settings->_cacheDir, $width); + if($conversionmgr) + $previewer->setConversionMgr($conversionmgr); + else + $previewer->setConverters($settings->_converters['preview']); + if(!$previewer->hasPreview($object)) + $previewer->createPreview($object); + + $file = $previewer->getFileName($object, $width).".png"; + if(!($fh = @fopen($file, 'rb'))) { + return $response->withJson(array('success'=>false, 'message'=>'', 'data'=>''), 500); + } + $stream = new \Slim\Http\Stream($fh); // create a stream instance for the response body + + return $response->withHeader('Content-Type', 'image/png') + ->withHeader('Content-Description', 'File Transfer') + ->withHeader('Content-Transfer-Encoding', 'binary') + ->withHeader('Content-Disposition', 'attachment; filename="preview-' . $document->getID() . "-" . $object->getVersion() . "-" . $width . ".png" . '"') + ->withHeader('Content-Length', $previewer->getFilesize($object)) + ->withBody($stream); + } + } + return $response->withStatus(403); + } /* }}} */ + + function fetch_doc($request, $response, $args) { /* {{{ */ + $logger = $this->container->logger; + $logger->log('Fetch doc '.$args['id'], PEAR_LOG_INFO); + return $response->withRedirect($request->getUri()->getBasePath().'/api/documents/'.$args['id'].'/download/', 302); + } /* }}} */ + + function documents_download($request, $response, $args) { /* {{{ */ + $dms = $this->container->dms; + $userobj = $this->container->userobj; + $settings = $this->container->config; + $conversionmgr = $this->container->conversionmgr; + $logger = $this->container->logger; + + if (!isset($args['id']) || !$args['id']) + return $response->withStatus(404); + + $logger->log('Download doc '.$args['id'], PEAR_LOG_INFO); + $document = $dms->getDocument($args['id']); + if($document) { + if($document->getAccessMode($userobj) >= M_READ) { + $lc = $document->getLatestContent(); + if($lc) { + if (pathinfo($document->getName(), PATHINFO_EXTENSION) == $lc->getFileType()) + $filename = $document->getName(); + else + $filename = $document->getName().$lc->getFileType(); + $file = $dms->contentDir . $lc->getPath(); + if(!($fh = @fopen($file, 'rb'))) { + return $response->withJson(array('success'=>false, 'message'=>'', 'data'=>''), 500); + } + $stream = new \Slim\Http\Stream($fh); // create a stream instance for the response body + + return $response->withHeader('Content-Type', $lc->getMimeType()) + ->withHeader('Content-Description', 'File Transfer') + ->withHeader('Content-Transfer-Encoding', 'binary') + ->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"') + ->withHeader('Content-Length', filesize($dms->contentDir . $lc->getPath())) + ->withHeader('Expires', '0') + ->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->withHeader('Pragma', 'no-cache') + ->withBody($stream); + } else { + return $response->withStatus(403); + } + } else + return $response->withStatus(404); + } else { + return $response->withStatus(500); + } + } /* }}} */ + + function documents_metadata($request, $response, $args) { /* {{{ */ + $dms = $this->container->dms; + $userobj = $this->container->userobj; + $settings = $this->container->config; + $conversionmgr = $this->container->conversionmgr; + $logger = $this->container->logger; + + if (!isset($args['id']) || !$args['id']) + return $response->withStatus(404); + + $document = $dms->getDocument($args['id']); + if($document) { + if($document->getAccessMode($userobj) >= M_READ) { + $lc = $document->getLatestContent(); + if($lc) { + return $response->withJson(array( + 'original_checksum'=>$lc->getChecksum(), + 'original_size'=>$lc->getFilesize(), + 'original_mimetype'=>$lc->getMimeType(), + 'has_archived_version'=>false + ), 200); + } else { + return $response->withStatus(403); + } + } else + return $response->withStatus(404); + } else { + return $response->withStatus(500); + } + } /* }}} */ + + function post_document($request, $response) { /* {{{ */ + $dms = $this->container->dms; + $userobj = $this->container->userobj; + $settings = $this->container->config; + $logger = $this->container->logger; + $fulltextservice = $this->container->fulltextservice; + $notifier = $this->container->notifier; + + if(!empty($settings->_extensions['paperless']['usehomefolder'])) { + if(!($mfolder = $userobj->getHomeFolder())) + $mfolder = $dms->getFolder($settings->_rootFolderID); + } elseif(!isset($settings->_extensions['paperless']['rootfolder']) || !($mfolder = $dms->getFolder($settings->_extensions['paperless']['rootfolder']))) + $mfolder = $dms->getFolder($settings->_rootFolderID); + if($mfolder) { + if($mfolder->getAccessMode($userobj) < M_READWRITE) + return $response->withStatus(403); + + $data = $request->getParsedBody(); + $logger->log(var_export($data, true), PEAR_LOG_INFO); + $uploadedFiles = $request->getUploadedFiles(); + if (count($uploadedFiles) == 0) { + return $response->withJson(getMLText("paperless_no_files_uploaded"), 400); + } + $origfilename = null; + $file_info = array_pop($uploadedFiles); + if ($origfilename == null) + $origfilename = $file_info->getClientFilename(); + if(!empty($data['title'])) + $docname = $data['title']; + else + $docname = $origfilename; + + /* Check if name already exists in the folder */ + if(!$settings->_enableDuplicateDocNames) { + if($mfolder->hasDocumentByName($docname)) { + return $response->withJson(getMLText("document_duplicate_name"), 409); + } + } + /* If several tags are set, they will all be saved individually in + * a parameter named 'tags'. This cannot be handled by php. It would + * require to use 'tags[]'. Hence, only the last tag will be taken into + * account. + */ + $cats = []; + if(!empty($data['tags'])) { + if($cat = $dms->getDocumentCategory((int) $data['tags'])) + $cats[] = $cat; + } +// $logger->log(var_export($cats, true), PEAR_LOG_INFO); + $userfiletmp = $file_info->file; + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $userfiletype = finfo_file($finfo, $userfiletmp); + $fileType = ".".pathinfo($origfilename, PATHINFO_EXTENSION); + finfo_close($finfo); + + $reviewers = array(); + $approvers = array(); + $reviewers["i"] = array(); + $reviewers["g"] = array(); + $approvers["i"] = array(); + $approvers["g"] = array(); + $workflow = null; + + $comment = ''; + $expires = null; + $owner = null; + $keywords = ''; + $sequence = 1; + $reqversion = null; + $version_comment = ''; + $attributes = array(); + $attributes_version = array(); + $notusers = array(); + $notgroups = array(); + + $controller = Controller::factory('AddDocument', array('dms'=>$dms, 'user'=>$userobj)); + $controller->setParam('documentsource', 'paperless'); + $controller->setParam('folder', $mfolder); + $controller->setParam('fulltextservice', $fulltextservice); + $controller->setParam('name', $docname); + $controller->setParam('comment', $comment); + $controller->setParam('expires', $expires); + $controller->setParam('keywords', $keywords); + $controller->setParam('categories', $cats); + $controller->setParam('owner', $userobj); + $controller->setParam('userfiletmp', $userfiletmp); + $controller->setParam('userfilename', $origfilename ? $origfilename : basename($userfiletmp)); + $controller->setParam('filetype', $fileType); + $controller->setParam('userfiletype', $userfiletype); + $controller->setParam('sequence', $sequence); + $controller->setParam('reviewers', $reviewers); + $controller->setParam('approvers', $approvers); + $controller->setParam('reqversion', $reqversion); + $controller->setParam('versioncomment', $version_comment); + $controller->setParam('attributes', $attributes); + $controller->setParam('attributesversion', $attributes_version); + $controller->setParam('workflow', $workflow); + $controller->setParam('notificationgroups', $notgroups); + $controller->setParam('notificationusers', $notusers); + $controller->setParam('maxsizeforfulltext', $settings->_maxSizeForFullText); + $controller->setParam('defaultaccessdocs', $settings->_defaultAccessDocs); + if(!$document = $controller()) { + $err = $controller->getErrorMsg(); + if(is_string($err)) + $errmsg = getMLText($err); + elseif(is_array($err)) { + $errmsg = getMLText($err[0], $err[1]); + } else { + $errmsg = $err; + } + $logger->log($errmsg, PEAR_LOG_NOTICE); + return $response->withJson(getMLText('paperless_upload_failed'), 500); + } else { + $logger->log(getMLText('paperless_upload_succeded'), PEAR_LOG_NOTICE); + /* Turn off for now, because file_info is not an array + if($controller->hasHook('cleanUpDocument')) { + $controller->callHook('cleanUpDocument', $document, $file_info); + } + */ + // Send notification to subscribers of folder. + if($notifier) { + $notifier->sendNewDocumentMail($document, $userobj); + } + if($settings->_removeFromDropFolder) { + if(file_exists($userfiletmp)) { + unlink($userfiletmp); + } + } + return $response->withJson('OK', 200); + } + } + return $response->withJson(getMLText('paperless_missing_target_folder'), 400); + } /* }}} */ + + function patch_documents($request, $response, $args) { /* {{{ */ + $dms = $this->container->dms; + $userobj = $this->container->userobj; + $settings = $this->container->config; + $conversionmgr = $this->container->conversionmgr; + $logger = $this->container->logger; + $fulltextservice = $this->container->fulltextservice; + + if (!isset($args['id']) || !$args['id']) + return $response->withStatus(404); + + $document = $dms->getDocument($args['id']); + if($document) { + $body = $request->getBody(); + if($data = json_decode($body, true)) { + if(isset($data['tags'])) { + $cats = []; + foreach($data['tags'] as $tagid) { + if($cat = $dms->getDocumentCategory($tagid)) { + $cats[] = $cat; + } + } + if(!$document->setCategories($cats)) + return $response->withStatus(500); + if($fulltextservice && ($index = $fulltextservice->Indexer())) { + $idoc = $fulltextservice->IndexedDocument($document); +// if(false !== $this->callHook('preIndexDocument', $document, $idoc)) { + $lucenesearch = $fulltextservice->Search(); + if($hit = $lucenesearch->getDocument((int) $document->getId())) { + $index->delete($hit->id); + } + $index->addDocument($idoc); + $index->commit(); +// } + } + } + } + } + return $response->withStatus(200); + } /* }}} */ + + function put_documents($request, $response, $args) { /* {{{ */ + $dms = $this->container->dms; + $userobj = $this->container->userobj; + $settings = $this->container->config; + $conversionmgr = $this->container->conversionmgr; + $logger = $this->container->logger; + $fulltextservice = $this->container->fulltextservice; + + if (!isset($args['id']) || !$args['id']) + return $response->withStatus(404); + + $document = $dms->getDocument($args['id']); + if($document) { + $body = $request->getBody(); + if($data = json_decode($body, true)) { + if(isset($data['tags'])) { + $cats = []; + foreach($data['tags'] as $tagid) { + if($cat = $dms->getDocumentCategory($tagid)) { + $cats[] = $cat; + } + } + if(!$document->setCategories($cats)) + return $response->withStatus(500); + if($fulltextservice && ($index = $fulltextservice->Indexer())) { + $idoc = $fulltextservice->IndexedDocument($document); +// if(false !== $this->callHook('preIndexDocument', $document, $idoc)) { + $lucenesearch = $fulltextservice->Search(); + if($hit = $lucenesearch->getDocument((int) $document->getId())) { + $index->delete($hit->id); + } + $index->addDocument($idoc); + $index->commit(); +// } + } + } + } + } + return $response->withJson($this->__getDocumentData($document), 200); + } /* }}} */ + + function delete_documents($request, $response, $args) { /* {{{ */ + $dms = $this->container->dms; + $userobj = $this->container->userobj; + $settings = $this->container->config; + $conversionmgr = $this->container->conversionmgr; + $logger = $this->container->logger; + $fulltextservice = $this->container->fulltextservice; + + if (!isset($args['id']) || !$args['id']) + return $response->withStatus(404); + + $document = $dms->getDocument($args['id']); + if($document) { + $controller = Controller::factory('RemoveDocument', array('dms'=>$dms, 'user'=>$userobj)); + $controller->setParam('document', $document); + $controller->setParam('fulltextservice', $fulltextservice); + if(!$controller()) { + $logger->log($controller->getErrorMsg(), PEAR_LOG_NOTICE); + return $response->withStatus(500); + } + } + return $response->withStatus(200); + } /* }}} */ +} /* }}} */ + +class SeedDMS_ExtPaperless_RestAPI_Auth { /* {{{ */ + + private $container; + + public function __construct($container) { + $this->container = $container; + } + + /** + * Example middleware invokable class + * + * @param \Psr\Http\Message\ServerRequestInterface $request PSR7 request + * @param \Psr\Http\Message\ResponseInterface $response PSR7 response + * @param callable $next Next middleware + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function __invoke($request, $response, $next) { /* {{{ */ + // $this->container has the DI + $dms = $this->container->dms; + $settings = $this->container->config; + $logger = $this->container->logger; + + /* Skip this middleware if the authentication was already successful */ + $userobj = null; + if($this->container->has('userobj')) + $userobj = $this->container->userobj; + + if($userobj) { + $response = $next($request, $response); + return $response; + } + + /* Pretent to be paperless ngx 1.10.0 with api version 2 */ + $response = $response->withHeader('x-api-version', '2')->withHeader('x-version', '1.10.0'); + + $logger->log("Invoke paperless middleware for method ".$request->getMethod()." on '".$request->getUri()->getPath()."'", PEAR_LOG_INFO); + if(!in_array($request->getUri()->getPath(), array('api/token/', 'api/'))) { + $userobj = null; + if(!empty($this->container->environment['HTTP_AUTHORIZATION'])) { + $tmp = explode(' ', $this->container->environment['HTTP_AUTHORIZATION'], 2); + switch($tmp[0]) { + case 'Token': + /* if jwtsecret is set, the token is expected to be a jwt */ + if(!empty($settings->_extensions['paperless']['jwtsecret'])) { + $token = new SeedDMS_JwtToken($settings->_extensions['paperless']['jwtsecret']); + if(!$tokenstr = $token->jwtDecode($tmp[1])) { + $logger->log("Could not decode jwt", PEAR_LOG_ERR); + return $response->withJson("Invalid token", 403); + } + $tmp = explode(':', json_decode($tokenstr, true)); + if($tmp[1] < time()) { + $logger->log("Jwt has expired at ".date('Y-m-d H:i:s', $tmp[1]), PEAR_LOG_ERR); + return $response->withJson("Token has expired", 403); + } else { + $logger->log("Token is valid till ".date('Y-m-d H:i:s', $tmp[1]), PEAR_LOG_DEBUG); + } + if(!($userobj = $dms->getUser((int) $tmp[0]))) { + $logger->log("No such user ".$tmp[0], PEAR_LOG_ERR); + return $response->withJson("No such user", 403); + } + $dms->setUser($userobj); + $this->container['userobj'] = $userobj; + $logger->log("Login with jwt as '".$userobj->getLogin()."' successful", PEAR_LOG_INFO); + } else { + if(!empty($settings->_apiKey) && !empty($settings->_apiUserId)) { + if($settings->_apiKey == $tmp[1]) { + if(!($userobj = $dms->getUser($settings->_apiUserId))) { + return $response->withStatus(403); + } + } else { + $logger->log("Login with apikey '".$tmp[1]."' failed", PEAR_LOG_INFO); + return $response->withStatus(403); + } + $dms->setUser($userobj); + $this->container['userobj'] = $userobj; + $logger->log("Login with apikey as '".$userobj->getLogin()."' successful", PEAR_LOG_INFO); + } + } + break; + case 'Basic': + $authenticator = $this->container->authenticator; + $kk = explode(':', base64_decode($tmp[1])); + $userobj = $authenticator->authenticate($kk[0], $kk[1]); + if(!$userobj) { + $logger->log("Login with basic authentication for '".$kk[0]."' failed", PEAR_LOG_INFO); + return $response->withStatus(403); + } + $dms->setUser($userobj); + $this->container['userobj'] = $userobj; + $logger->log("Login with basic authentication as '".$userobj->getLogin()."' successful", PEAR_LOG_INFO); + break; + } + } + } else { + /* Set userobj to keep other middlewares for authentication from running */ + $this->container['userobj'] = true; + } + $response = $next($request, $response); + return $response; + } /* }}} */ +} /* }}} */ + +/** + * Class containing methods which adds additional routes to the RestAPI + * + * @author Uwe Steinmann + * @package SeedDMS + * @subpackage paperless + */ +class SeedDMS_ExtPaperless_RestAPI { /* {{{ */ + + /** + * Hook for adding additional routes to the RestAPI + * + * @param object $app instance of \Slim\App + * @return void + */ + public function addMiddleware($app) { /* {{{ */ + $container = $app->getContainer(); + $app->add(new SeedDMS_ExtPaperless_RestAPI_Auth($container)); + } /* }}} */ + + /** + * Hook for adding additional routes to the RestAPI + * + * @param object $app instance of \Slim\App + * @return void + */ + public function addRoute($app) { /* {{{ */ + $app->get('/api/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':api'); + /* /api/token/ is actually a get, but paperless_app calls it to check for ngx */ + $app->post('/api/token/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':token'); + $app->get('/api/token/', function($request, $response) use ($app) { + return $response->withStatus(405); + }); + $app->get('/api/tags/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':tags'); + $app->get('/api/documents/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':documents'); + $app->get('/api/correspondents/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':correspondents'); + $app->get('/api/document_types/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':document_types'); + $app->get('/api/saved_views/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':saved_views'); + $app->get('/api/storage_paths/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':storage_paths'); + $app->post('/api/documents/post_document/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':post_document'); + $app->get('/api/documents/{id}/preview/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':documents_download'); + $app->get('/api/documents/{id}/thumb/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':documents_thumb'); + $app->get('/fetch/thumb/{id}', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':fetch_thumb'); + $app->get('/api/documents/{id}/download/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':documents_download'); + $app->get('/api/documents/{id}/metadata/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':documents_metadata'); + $app->get('/fetch/doc/{id}', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':fetch_doc'); + $app->patch('/api/documents/{id}/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':patch_documents'); + $app->put('/api/documents/{id}/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':put_documents'); + $app->delete('/api/documents/{id}/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':delete_documents'); + $app->get('/api/search/autocomplete/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':autocomplete'); + $app->get('/api/ui_settings/', \SeedDMS_ExtPaperless_RestAPI_Controller::class.':ui_settings'); + + return null; + } /* }}} */ + +} /* }}} */ + diff --git a/conf.php b/conf.php new file mode 100644 index 0000000..1ca6398 --- /dev/null +++ b/conf.php @@ -0,0 +1,46 @@ + 'Extents the RestAPI with method for Paperless', + 'description' => 'This extension adds additional routes to make it behave like a paperless server', + 'disable' => false, + 'version' => '1.0.0', + 'releasedate' => '2022-12-07', + 'author' => array('name'=>'Uwe Steinmann', 'email'=>'uwe@steinmann.cx', 'company'=>'MMK GmbH'), + 'config' => array( + 'rootfolder' => array( + 'title'=>'Folder used as root folder', + 'help'=>'This is the folder used as the base folder. Uploaded documents will be saved in this folder and all documents listed will result in fulltext search below this folder.', + 'type'=>'select', + 'internal'=>'folders', + ), + 'usehomefolder' => array( + 'title'=>'Use the home folder as root folder', + 'type'=>'checkbox', + 'help'=>"Enable, if the user's home folder shall be used instead of the configured root folder.", + ), + 'jwtsecret' => array( + 'title'=>'Secret for JSON Web Token', + 'help'=>'This is used for creating a token which is needed to authenticate by token', + 'type'=>'password', + ), + 'inboxtags' => array( + 'title'=>'Categories treated as inbox tag', + 'help'=>'These categories are marked as inbox tag when the list of tags is retrieved.', + 'type'=>'select', + 'internal'=>'categories', + ), + ), + 'constraints' => array( + 'depends' => array('php' => '5.6.40-', 'seeddms' => array('5.1.29-5.1.99', '6.0.22-6.0.99')), + ), + 'icon' => 'icon.svg', + 'changelog' => 'changelog.md', + 'class' => array( + 'file' => 'class.paperless.php', + 'name' => 'SeedDMS_ExtPaperless' + ), + 'language' => array( + 'file' => 'lang.php', + ), +); +?> diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..2589435 --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lang.php b/lang.php new file mode 100644 index 0000000..cbaaab0 --- /dev/null +++ b/lang.php @@ -0,0 +1,12 @@ +'Upload succeded', + 'paperless_upload_failed'=>'Upload failed', + 'paperless_missing_target_folder'=>'Missing target folder', +); +$__lang['de_DE'] = array( + 'paperless_upload_succeded'=>'Erfolgreich hochgeladen', + 'paperless_upload_failed'=>'Hochladen fehlgeschlagen', + 'paperless_missing_target_folder'=>'Zielordner nicht vorhanden', +); +