<?php /** * Copyright Maarch since 2008 under licence GPLv3. * See LICENCE.txt file at the root folder for more details. * This file is part of Maarch software. * */ /** * @brief Resource Controller * @author dev@maarch.org */ namespace Document\controllers; use Attachment\controllers\AttachmentController; use Docserver\models\AdrModel; use Email\controllers\EmailController; use Group\controllers\PrivilegeController; use Respect\Validation\Validator; use setasign\Fpdi\Tcpdf\Fpdi; use SrcCore\models\CoreConfigModel; use Attachment\models\AttachmentModel; use Docserver\controllers\DocserverController; use Docserver\models\DocserverModel; use Document\models\DocumentModel; use Slim\Http\Request; use Slim\Http\Response; use SrcCore\models\DatabaseModel; use SrcCore\models\ValidatorModel; use User\models\UserModel; use History\controllers\HistoryController; use Workflow\models\WorkflowModel; class DocumentController { const ACTIONS = [1 => 'VAL', 2 => 'REF']; const MODES = ['visa', 'sign', 'note']; public function get(Request $request, Response $response) { $queryParams = $request->getQueryParams(); $queryParams['offset'] = empty($queryParams['offset']) ? 0 : (int)$queryParams['offset']; $queryParams['limit'] = empty($queryParams['limit']) ? 0 : (int)$queryParams['limit']; $userId = $GLOBALS['id']; if (!empty($queryParams['userId'])) { if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents'])) { return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']); } if (!Validator::intVal()->notEmpty()->validate($queryParams['userId'])) { return $response->withStatus(400)->withJson(['errors' => 'QueryParams userId is not an integer']); } $userId = $queryParams['userId']; } $substitutedUsers = UserModel::get(['select' => ['id'], 'where' => ['substitute = ?'], 'data' => [$userId]]); $users = [$userId]; foreach ($substitutedUsers as $value) { $users[] = $value['id']; } $workflowSelect = "SELECT id FROM workflows ws WHERE workflows.main_document_id = main_document_id AND process_date IS NULL AND status IS NULL ORDER BY \"order\" LIMIT 1"; $where = ['user_id in (?)', "(id) in ({$workflowSelect})"]; $data = [$users]; $countWorkflows = WorkflowModel::get([ 'select' => ['count(mode)', 'mode'], 'where' => $where, 'data' => $data, 'groupBy' => ['mode'] ]); if (!empty($queryParams['mode']) && in_array($queryParams['mode'], DocumentController::MODES)) { $where[] = 'mode = ?'; $data[] = $queryParams['mode']; } $workflows = WorkflowModel::get([ 'select' => ['main_document_id', 'mode', 'user_id'], 'where' => $where, 'data' => $data ]); $documentIds = []; $workflowsShortcut = []; foreach ($workflows as $workflow) { $documentIds[] = $workflow['main_document_id']; $workflowsShortcut[$workflow['main_document_id']] = $workflow; } $documents = []; if (!empty($documentIds)) { $where = ['id in (?)']; $data = [$documentIds]; if (!empty($queryParams['search'])) { $where[] = '(reference ilike ? OR translate(title, \'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûýýþÿŔŕ\', \'aaaaaaaceeeeiiiidnoooooouuuuybsaaaaaaaceeeeiiiidnoooooouuuyybyrr\') ilike translate(?, \'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûýýþÿŔŕ\', \'aaaaaaaceeeeiiiidnoooooouuuuybsaaaaaaaceeeeiiiidnoooooouuuyybyrr\'))'; $data[] = "%{$queryParams['search']}%"; $data[] = "%{$queryParams['search']}%"; } $documents = DocumentModel::get([ 'select' => ['id', 'title', 'reference', 'count(1) OVER()'], 'where' => $where, 'data' => $data, 'limit' => $queryParams['limit'], 'offset' => $queryParams['offset'], 'orderBy' => ['creation_date desc'] ]); } $count = ['visa' => 0, 'sign' => 0, 'note' => 0, 'current' => empty($documents[0]['count']) ? 0 : $documents[0]['count']]; foreach ($documents as $key => $document) { unset($documents[$key]['count']); $documents[$key]['mode'] = $workflowsShortcut[$document['id']]['mode']; $documents[$key]['owner'] = $workflowsShortcut[$document['id']]['user_id'] == $userId; } foreach ($countWorkflows as $mode) { $count[$mode['mode']] = $mode['count']; } return $response->withJson(['documents' => $documents, 'count' => $count]); } public function getById(Request $request, Response $response, array $args) { if (!DocumentController::hasRightById(['id' => $args['id'], 'userId' => $GLOBALS['id']]) && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents'])) { return $response->withStatus(403)->withJson(['errors' => 'Document out of perimeter']); } $document = DocumentModel::getById(['select' => ['*'], 'id' => $args['id']]); if (empty($document)) { return $response->withStatus(400)->withJson(['errors' => 'Document does not exist']); } $adr = AdrModel::getDocumentsAdr([ 'select' => ['count(1)'], 'where' => ['main_document_id = ?', 'type != ?'], 'data' => [$args['id'], 'DOC'] ]); if (empty($adr[0]['count'])) { return $response->withStatus(400)->withJson(['errors' => 'Document thumbnails do not exist']); } $formattedDocument = [ 'id' => $document['id'], 'title' => $document['title'], 'reference' => $document['reference'], 'description' => $document['description'], 'sender' => $document['sender'], 'creationDate' => $document['creation_date'], 'modificationDate' => $document['modification_date'], 'pages' => $adr[0]['count'] ]; if (!empty($document['deadline'])) { $date = new \DateTime($document['deadline']); $formattedDocument['deadline'] = $date->format('d-m-Y H:i'); } $formattedDocument['metadata'] = []; $metadata = json_decode($document['metadata'], true); if (is_array($metadata)) { foreach ($metadata as $key => $value) { $formattedDocument['metadata'][] = ['label' => $key, 'value' => $value]; } } $workflow = WorkflowModel::getByDocumentId(['select' => ['user_id', 'mode', 'process_date'], 'documentId' => $args['id'], 'orderBy' => ['"order"']]); $currentFound = false; foreach ($workflow as $value) { if (!empty($value['process_date'])) { $date = new \DateTime($document['process_date']); $value['process_date'] = $date->format('d-m-Y H:i'); } $formattedDocument['workflow'][] = [ 'userId' => $value['user_id'], 'userDisplay' => UserModel::getLabelledUserById(['id' => $value['user_id']]), 'mode' => $value['mode'], 'processDate' => $value['process_date'], 'current' => !$currentFound && empty($value['process_date']) ]; if (empty($value['process_date'])) { $currentFound = true; } } $formattedDocument['attachments'] = []; $attachments = AttachmentModel::getByDocumentId(['select' => ['id', 'title'], 'documentId' => $args['id']]); foreach ($attachments as $attachment) { $pagesCount = 0; $adr = AdrModel::getAttachmentsAdr([ 'select' => ['count(1)'], 'where' => ['attachment_id = ?', 'type != ?'], 'data' => [$attachment['id'], 'ATTACH'] ]); if (!empty($adr[0]['count'])) { $pagesCount = $adr[0]['count']; } $formattedDocument['attachments'][] = [ 'id' => $attachment['id'], 'title' => $attachment['title'], 'pages' => $pagesCount ]; } HistoryController::add([ 'code' => 'OK', 'objectType' => 'main_documents', 'objectId' => $args['id'], 'type' => 'VIEW', 'message' => "{documentViewed} : {$document['title']}" ]); return $response->withJson(['document' => $formattedDocument]); } public function getContent(Request $request, Response $response, array $args) { if (!DocumentController::hasRightById(['id' => $args['id'], 'userId' => $GLOBALS['id']]) && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents'])) { return $response->withStatus(403)->withJson(['errors' => 'Document out of perimeter']); } $adr = AdrModel::getDocumentsAdr([ 'select' => ['path', 'filename', 'fingerprint'], 'where' => ['main_document_id = ?', 'type = ?'], 'data' => [$args['id'], 'DOC'] ]); if (empty($adr[0])) { return $response->withJson(['encodedDocument' => null]); } $docserver = DocserverModel::getByType(['type' => 'DOC', 'select' => ['path']]); if (empty($docserver['path']) || !file_exists($docserver['path'])) { return $response->withStatus(400)->withJson(['errors' => 'Docserver does not exist']); } $pathToDocument = $docserver['path'] . $adr[0]['path'] . $adr[0]['filename']; if (!is_file($pathToDocument)) { return $response->withStatus(404)->withJson(['errors' => 'Document not found on docserver']); } $fingerprint = DocserverController::getFingerPrint(['path' => $pathToDocument]); if ($adr[0]['fingerprint'] != $fingerprint) { return $response->withStatus(400)->withJson(['errors' => 'Fingerprints do not match']); } $document = DocumentModel::getById(['select' => ['title'], 'id' => $args['id']]); HistoryController::add([ 'code' => 'OK', 'objectType' => 'main_documents', 'objectId' => $args['id'], 'type' => 'VIEW', 'message' => "{documentViewed} : {$document['title']}" ]); return $response->withJson(['encodedDocument' => base64_encode(file_get_contents($pathToDocument))]); } public function create(Request $request, Response $response) { if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_documents'])) { return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']); } $body = $request->getParsedBody(); if (empty($body)) { return $response->withStatus(400)->withJson(['errors' => 'Body is not set or empty']); } elseif (!Validator::notEmpty()->validate($body['encodedDocument'])) { return $response->withStatus(400)->withJson(['errors' => 'Body encodedDocument is empty']); } elseif (!Validator::stringType()->notEmpty()->validate($body['title'])) { return $response->withStatus(400)->withJson(['errors' => 'Body title is empty or not a string']); } elseif (!Validator::stringType()->notEmpty()->validate($body['sender'])) { return $response->withStatus(400)->withJson(['errors' => 'Body sender is empty or not a string']); } elseif (!Validator::arrayType()->notEmpty()->validate($body['workflow'])) { return $response->withStatus(400)->withJson(['errors' => 'Body workflow is empty or not an array']); } $body['attachments'] = empty($body['attachments']) ? [] : $body['attachments']; foreach ($body['attachments'] as $key => $attachment) { if (!Validator::notEmpty()->validate($attachment['encodedDocument'])) { return $response->withStatus(400)->withJson(['errors' => "Body attachments[{$key}] encodedDocument is empty"]); } elseif (!Validator::stringType()->notEmpty()->validate($attachment['title'])) { return $response->withStatus(400)->withJson(['errors' => "Body attachments[{$key}] title is empty"]); } } foreach ($body['workflow'] as $key => $workflow) { if (!empty($workflow['processingUser'])) { $processingUser = UserModel::getByLogin(['select' => ['id'], 'login' => strtolower($workflow['processingUser'])]); } elseif (!empty($workflow['userId'])) { $processingUser = UserModel::getById(['select' => ['id'], 'id' => $workflow['userId']]); } if (empty($processingUser)) { return $response->withStatus(400)->withJson(['errors' => "Body workflow[{$key}] processingUser/userId is empty or does not exist"]); } elseif (!Validator::stringType()->notEmpty()->validate($workflow['mode']) || !in_array($workflow['mode'], DocumentController::MODES)) { return $response->withStatus(400)->withJson(['errors' => "Body workflow[{$key}] mode is empty or not a string in ('visa', 'sign', 'note')"]); } $body['workflow'][$key]['userId'] = $processingUser['id']; } $encodedDocument = DocumentController::getEncodedDocumentFromEncodedZip(['encodedZipDocument' => $body['encodedDocument']]); if (!empty($encodedDocument['errors'])) { return $response->withStatus(500)->withJson(['errors' => $encodedDocument['errors']]); } $storeInfos = DocserverController::storeResourceOnDocServer([ 'encodedFile' => $encodedDocument['encodedDocument'], 'format' => 'pdf', 'docserverType' => 'DOC' ]); if (!empty($storeInfos['errors'])) { return $response->withStatus(500)->withJson(['errors' => $storeInfos['errors']]); } DatabaseModel::beginTransaction(); $id = DocumentModel::create([ 'title' => $body['title'], 'reference' => empty($body['reference']) ? null : $body['reference'], 'description' => empty($body['description']) ? null : $body['description'], 'sender' => $body['sender'], 'deadline' => empty($body['deadline']) ? null : $body['deadline'], 'metadata' => empty($body['metadata']) ? '{}' : json_encode($body['metadata']) ]); AdrModel::createDocumentAdr([ 'documentId' => $id, 'type' => 'DOC', 'path' => $storeInfos['path'], 'filename' => $storeInfos['filename'], 'fingerprint' => $storeInfos['fingerprint'] ]); foreach ($body['workflow'] as $key => $workflow) { WorkflowModel::create([ 'userId' => $workflow['userId'], 'mainDocumentId' => $id, 'mode' => $workflow['mode'], 'order' => $key + 1 ]); } foreach ($body['attachments'] as $key => $value) { $value['mainDocumentId'] = $id; $attachment = AttachmentController::create($value); if (!empty($attachment['errors'])) { DatabaseModel::rollbackTransaction(); return $response->withStatus(500)->withJson(['errors' => "An error occured for attachment {$key} : {$attachment['errors']}"]); } } HistoryController::add([ 'code' => 'OK', 'objectType' => 'main_documents', 'objectId' => $id, 'type' => 'CREATION', 'message' => "{documentAdded} : {$body['title']}" ]); DatabaseModel::commitTransaction(); EmailController::sendNotificationToNextUserInWorkflow(['documentId' => $id, 'userId' => $GLOBALS['id']]); $configPath = CoreConfigModel::getConfigPath(); exec("php src/app/convert/scripts/ThumbnailScript.php '{$configPath}' {$id} 'document' '{$GLOBALS['id']}' > /dev/null &"); return $response->withJson(['id' => $id]); } public function setAction(Request $request, Response $response, array $args) { if (!DocumentController::hasRightById(['id' => $args['id'], 'userId' => $GLOBALS['id']])) { return $response->withStatus(403)->withJson(['errors' => 'Document out of perimeter']); } $currentUser = UserModel::getById(['id' => $GLOBALS['id'], 'select' => ['substitute']]); if (!empty($currentUser['substitute'])) { return $response->withStatus(403)->withJson(['errors' => 'User can not make action with substituted account']); } if (empty(DocumentController::ACTIONS[$args['actionId']])) { return $response->withStatus(400)->withJson(['errors' => 'Action does not exist']); } $workflow = WorkflowModel::getCurrentStep(['select' => ['id', 'mode'], 'documentId' => $args['id']]); $body = $request->getParsedBody(); if (!empty($body['signatures'])) { foreach ($body['signatures'] as $signature) { foreach (['encodedImage', 'width', 'positionX', 'positionY', 'page', 'type'] as $value) { if (!isset($signature[$value])) { return $response->withStatus(400)->withJson(['errors' => $value . ' is empty']); } } } $adr = AdrModel::getDocumentsAdr([ 'select' => ['path', 'filename'], 'where' => ['main_document_id = ?', 'type = ?'], 'data' => [$args['id'], 'DOC'] ]); if (empty($adr)) { return $response->withStatus(400)->withJson(['errors' => 'Document does not exist']); } $docserver = DocserverModel::getByType(['type' => 'DOC', 'select' => ['path']]); if (empty($docserver['path']) || !file_exists($docserver['path'])) { return $response->withStatus(400)->withJson(['errors' => 'Docserver does not exist']); } $pathToDocument = $docserver['path'] . $adr[0]['path'] . $adr[0]['filename']; if (!is_file($pathToDocument) || !is_readable($pathToDocument)) { return $response->withStatus(404)->withJson(['errors' => 'Document not found on docserver or not readable']); } $tmpPath = CoreConfigModel::getTmpPath(); $tmpFilename = $tmpPath . $GLOBALS['id'] . '_' . rand() . '_' . $adr[0]['filename']; copy($pathToDocument, $tmpFilename); $pdf = new Fpdi('P'); $nbPages = $pdf->setSourceFile($tmpFilename); $pdf->setPrintHeader(false); for ($i = 1; $i <= $nbPages; $i++) { $page = $pdf->importPage($i); $size = $pdf->getTemplateSize($page); $pdf->AddPage($size['orientation'], $size); $pdf->useImportedPage($page); $pdf->SetAutoPageBreak(false, 0); $pdf->SetMargins(0, 0, 0); $pdf->SetAutoPageBreak(false, 0); foreach ($body['signatures'] as $signature) { if ($signature['page'] == $i) { if ($signature['positionX'] == 0 && $signature['positionY'] == 0) { $signWidth = $size['width']; $signPosX = 0; $signPosY = 0; } else { $signWidth = $size['width'] / 4; $signPosX = ($signature['positionX'] * $size['width']) / 100; $signPosY = ($signature['positionY'] * $size['height']) / 100; } if ($signature['type'] == 'SVG') { $image = str_replace('data:image/svg+xml;base64,', '', $signature['encodedImage']); $image = base64_decode($image); if ($image === false) { return $response->withStatus(400)->withJson(['errors' => 'base64_decode failed']); } $imageTmpPath = $tmpPath . $GLOBALS['id'] . '_' . rand() . '_writing.svg'; file_put_contents($imageTmpPath, $image); $pdf->ImageSVG($imageTmpPath, $signPosX, $signPosY, $signWidth); } else { $image = base64_decode($signature['encodedImage']); if ($image === false) { return $response->withStatus(400)->withJson(['errors' => 'base64_decode failed']); } $imageTmpPath = $tmpPath . $GLOBALS['id'] . '_' . rand() . '_writing.png'; file_put_contents($imageTmpPath, $image); $pdf->Image($imageTmpPath, $signPosX, $signPosY, $signWidth); } } } } if (DocumentController::ACTIONS[$args['actionId']] == 'VAL' && $workflow['mode'] == 'sign') { $loadedXml = CoreConfigModel::getConfig(); if ($loadedXml->electronicSignature->enable == 'true') { $certPath = realpath((string)$loadedXml->electronicSignature->certPath); $privateKeyPath = realpath((string)$loadedXml->electronicSignature->privateKeyPath); if (is_file($certPath) && is_file($privateKeyPath)) { $certificate = 'file://' . $certPath; $privateKey = 'file://' . $privateKeyPath; $info = [ 'Name' => (string)$loadedXml->electronicSignature->certInfo->name, 'Location' => (string)$loadedXml->electronicSignature->certInfo->location, 'Reason' => (string)$loadedXml->electronicSignature->certInfo->reason, 'ContactInfo' => (string)$loadedXml->electronicSignature->certInfo->contactInfo ]; $pdf->setSignature($certificate, $privateKey, (string)$loadedXml->electronicSignature->password, '', 2, $info); } else { return $response->withStatus(400)->withJson(['errors' => 'certPath or privateKeyPath is not valid']); } } } $fileContent = $pdf->Output('', 'S'); $storeInfos = DocserverController::storeResourceOnDocServer([ 'encodedFile' => base64_encode($fileContent), 'format' => 'pdf', 'docserverType' => 'DOC' ]); if (!empty($storeInfos['errors'])) { return $response->withStatus(500)->withJson(['errors' => $storeInfos['errors']]); } unlink($pathToDocument); AdrModel::deleteDocumentAdr([ 'where' => ['main_document_id = ?', 'type = ?'], 'data' => [$args['id'], 'DOC'] ]); AdrModel::createDocumentAdr([ 'documentId' => $args['id'], 'type' => 'DOC', 'path' => $storeInfos['path'], 'filename' => $storeInfos['filename'], 'fingerprint' => $storeInfos['fingerprint'] ]); } $set = ['process_date' => 'CURRENT_TIMESTAMP', 'status' => DocumentController::ACTIONS[$args['actionId']]]; if (!empty($body['note'])) { $set['note'] = $body['note']; } WorkflowModel::update([ 'set' => $set, 'where' => ['id = ?'], 'data' => [$workflow['id']] ]); if (DocumentController::ACTIONS[$args['actionId']] == 'REF') { WorkflowModel::update([ 'set' => ['status' => 'END'], 'where' => ['main_document_id = ?', 'process_date is null'], 'data' => [$args['id']] ]); } AdrModel::deleteDocumentAdr([ 'where' => ['main_document_id = ?', 'type != ?'], 'data' => [$args['id'], 'DOC'] ]); $configPath = CoreConfigModel::getConfigPath(); exec("php src/app/convert/scripts/ThumbnailScript.php '{$configPath}' {$args['id']} 'document' '{$GLOBALS['id']}' > /dev/null &"); EmailController::sendNotificationToNextUserInWorkflow(['documentId' => $args['id'], 'userId' => $GLOBALS['id']]); HistoryController::add([ 'code' => 'OK', 'objectType' => 'main_documents', 'objectId' => $args['id'], 'type' => 'ACTION', 'message' => "{actionDone} : " . DocumentController::ACTIONS[$args['actionId']], 'data' => ['actionId' => $args['actionId']] ]); return $response->withJson(['success' => 'success']); } public function getThumbnailContent(Request $request, Response $response, array $args) { if (!DocumentController::hasRightById(['id' => $args['id'], 'userId' => $GLOBALS['id']])) { return $response->withStatus(403)->withJson(['errors' => 'Document out of perimeter']); } $adr = AdrModel::getDocumentsAdr([ 'select' => ['path', 'filename'], 'where' => ['main_document_id = ?', 'type = ?'], 'data' => [$args['id'], 'TNL' . $args['page']] ]); $docserver = DocserverModel::getByType(['type' => 'DOC', 'select' => ['path']]); if (empty($docserver['path']) || !file_exists($docserver['path'])) { return $response->withStatus(400)->withJson(['errors' => 'Docserver does not exist']); } $pathToThumbnail = $docserver['path'] . $adr[0]['path'] . $adr[0]['filename']; if (!is_file($pathToThumbnail) || !is_readable($pathToThumbnail)) { return $response->withStatus(400)->withJson(['errors' => 'Document not found on docserver or not readable']); } $fileContent = file_get_contents($pathToThumbnail); if ($fileContent === false) { return $response->withStatus(404)->withJson(['errors' => 'Thumbnail not found on docserver']); } $base64Content = base64_encode($fileContent); return $response->withJson(['fileContent' => $base64Content]); } public static function hasRightById(array $args) { ValidatorModel::notEmpty($args, ['id', 'userId']); ValidatorModel::intVal($args, ['id', 'userId']); $workflow = WorkflowModel::getCurrentStep(['select' => ['user_id'], 'documentId' => $args['id']]); if (empty($workflow)) { return false; } if ($workflow['user_id'] != $args['userId']) { $user = UserModel::getById(['id' => $workflow['user_id'], 'select' => ['substitute']]); if ($user['substitute'] != $args['userId']) { return false; } } return true; } public static function getEncodedDocumentFromEncodedZip(array $args) { ValidatorModel::notEmpty($args, ['encodedZipDocument']); ValidatorModel::stringType($args, ['encodedZipDocument']); $tmpPath = CoreConfigModel::getTmpPath(); $zipDocumentOnTmp = $tmpPath . mt_rand() . '_parapheur.zip'; file_put_contents($zipDocumentOnTmp, base64_decode($args['encodedZipDocument'])); $zipArchive = new \ZipArchive(); $open = $zipArchive->open($zipDocumentOnTmp); if ($open != true) { return ['errors' => "getDocumentFromEncodedZip : $open"]; } $dirOnTmp = $tmpPath . mt_rand() . '_parapheur'; if (!@$zipArchive->extractTo($dirOnTmp)) { return ['errors' => "getDocumentFromEncodedZip : Extract failed"]; } $filesOnTmp = scandir($dirOnTmp); foreach ($filesOnTmp as $fileOnTmp) { if ($fileOnTmp != '.' && $fileOnTmp != '..') { $base64Content = base64_encode(file_get_contents("{$dirOnTmp}/{$fileOnTmp}")); unlink($zipDocumentOnTmp); return ['encodedDocument' => $base64Content]; } } return ['errors' => "getDocumentFromEncodedZip : No document was found in Zip"]; } }