1: <?php
2: /**
3: * This code is licensed under AGPLv3 license or Afterlogic Software License
4: * if commercial version of the product was purchased.
5: * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
6: */
7:
8: namespace Aurora\Modules\OfficeDocumentEditor;
9:
10: use Afterlogic\DAV\FS\File;
11: use Afterlogic\DAV\FS\Directory;
12: use Afterlogic\DAV\FS\Permission;
13: use Aurora\Api;
14: use Afterlogic\DAV\Server;
15: use Aurora\System\Application;
16: use Aurora\Modules\Core\Module as CoreModule;
17: use Aurora\Modules\Files\Module as FilesModule;
18: use Aurora\Modules\Files\Classes\FileItem;
19: use Aurora\Modules\OfficeDocumentEditor\Exceptions\Exception;
20: use Aurora\System\Enums\FileStorageType;
21:
22: use function Sabre\Uri\split;
23:
24: /**
25: * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
26: * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
27: * @copyright Copyright (c) 2023, Afterlogic Corp.
28: *
29: * @package Modules
30: */
31: class Module extends \Aurora\System\Module\AbstractModule
32: {
33: public $ExtsSpreadsheet = [
34: ".xls",
35: ".xlsx",
36: ".xlsm",
37: ".xlt",
38: ".xltx",
39: ".xltm",
40: ".ods",
41: ".fods",
42: ".ots",
43: ".csv"
44: ];
45: public $ExtsPresentation = [
46: ".pps",
47: ".ppsx",
48: ".ppsm",
49: ".ppt",
50: ".pptx",
51: ".pptm",
52: ".pot",
53: ".potx",
54: ".potm",
55: ".odp",
56: ".fodp",
57: ".otp"
58: ];
59: public $ExtsDocument = [
60: ".doc",
61: ".docx",
62: ".docm",
63: ".dot",
64: ".dotx",
65: ".dotm",
66: ".odt",
67: ".fodt",
68: ".ott",
69: ".rtf",
70: ".txt",
71: ".html",
72: ".htm",
73: ".mht",
74: ".pdf",
75: ".djvu",
76: ".fb2",
77: ".epub",
78: ".xps"
79: ];
80:
81:
82: /**
83: * Initializes module.
84: *
85: * @ignore
86: */
87: public function init()
88: {
89: $this->aErrors = [
90: Enums\ErrorCodes::ExtensionCannotBeConverted => $this->i18N('ERROR_EXTENSION_CANNOT_BE_CONVERTED')
91: ];
92:
93: $this->AddEntries([
94: 'editor' => 'EntryEditor',
95: 'ode-callback' => 'EntryCallback'
96: ]);
97:
98: $this->subscribeEvent('System::RunEntry::before', [$this, 'onBeforeFileViewEntry'], 10);
99: $this->subscribeEvent('Files::GetFile', [$this, 'onGetFile'], 10);
100: $this->subscribeEvent('Files::GetItems', [$this, 'onGetItems'], 20000);
101: $this->subscribeEvent('Files::GetFileInfo::after', [$this, 'onAfterGetFileInfo'], 20000);
102: $this->subscribeEvent('AddToContentSecurityPolicyDefault', [$this, 'onAddToContentSecurityPolicyDefault']);
103: }
104:
105: public function GetSettings()
106: {
107: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
108:
109: return [
110: 'ExtensionsToView' => $this->getOfficeExtensions()
111: ];
112: }
113:
114: protected function getExtensionsToView()
115: {
116: return $this->getConfig('ExtensionsToView', []);
117: }
118:
119: protected function getExtensionsToConvert()
120: {
121: return $this->getConfig('ExtensionsToConvert', []);
122: }
123:
124: protected function getExtensionsToEdit()
125: {
126: return $this->getConfig('ExtensionsToEdit', []);
127: }
128:
129: protected function getOfficeExtensions()
130: {
131: return array_merge(
132: $this->getExtensionsToView(),
133: $this->getExtensionsToEdit(),
134: array_keys($this->getExtensionsToConvert())
135: );
136: }
137:
138: protected function getDocumentType($filename)
139: {
140: $ext = strtolower('.' . pathinfo($filename, PATHINFO_EXTENSION));
141:
142: if (in_array($ext, $this->ExtsDocument)) {
143: return "word";
144: }
145: if (in_array($ext, $this->ExtsSpreadsheet)) {
146: return "cell";
147: }
148: if (in_array($ext, $this->ExtsPresentation)) {
149: return "slide";
150: }
151: return "";
152: }
153:
154: protected function isReadOnlyDocument($filename)
155: {
156: $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
157:
158: return in_array($ext, $this->getExtensionsToView());
159: }
160:
161: protected function documentCanBeEdited($filename)
162: {
163: $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
164:
165: return in_array($ext, $this->getExtensionsToEdit());
166: }
167:
168: protected function documentCanBeConverted($sFilename)
169: {
170: $ext = strtolower(pathinfo($sFilename, PATHINFO_EXTENSION));
171:
172: return in_array($ext, array_keys($this->getExtensionsToConvert()));
173: }
174:
175: /**
176: * @param string $sFileName = ''
177: * @return bool
178: */
179: public function isOfficeDocument($sFileName = '')
180: {
181: $sExtensions = implode('|', $this->getOfficeExtensions());
182: return !!preg_match('/\.(' . $sExtensions . ')$/', strtolower(trim($sFileName)));
183: }
184:
185: protected function isTrustedRequest()
186: {
187: return true; // TODO: find another way to protect dowmload url
188:
189: $bResult = false;
190:
191: $sTrustedServerHost = $this->getConfig('TrustedServerHost', '');
192: if (empty($sTrustedServerHost)) {
193: $bResult = true;
194: } else {
195: if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
196: $ip = $_SERVER['HTTP_CLIENT_IP'];
197: } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
198: $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
199: } else {
200: $ip = $_SERVER['REMOTE_ADDR'];
201: }
202:
203: $bResult = $sTrustedServerHost === $ip;
204: }
205:
206: return $bResult;
207: }
208:
209: /**
210: *
211: * @param type $aArguments
212: * @param type $aResult
213: */
214: public function onBeforeFileViewEntry(&$aArguments, &$aResult)
215: {
216: $aEntries = [
217: 'download-file',
218: 'file-cache',
219: 'mail-attachment'
220: ];
221: if (in_array($aArguments['EntryName'], $aEntries)) {
222: $sEntry = (string) \Aurora\System\Router::getItemByIndex(0, '');
223: $sHash = (string) \Aurora\System\Router::getItemByIndex(1, '');
224: $sAction = (string) \Aurora\System\Router::getItemByIndex(2, '');
225:
226: $aValues = Api::DecodeKeyValues($sHash);
227:
228: $sFileName = isset($aValues['FileName']) ? urldecode($aValues['FileName']) : '';
229: if (empty($sFileName)) {
230: $sFileName = isset($aValues['Name']) ? urldecode($aValues['Name']) : '';
231: }
232: if ($sAction === 'view' && $this->isOfficeDocument($sFileName) && !isset($aValues['AuthToken'])) {
233: $sViewerUrl = './?editor=' . urlencode($sEntry .'/' . $sHash . '/' . $sAction . '/' . time());
234: \header('Location: ' . $sViewerUrl);
235: } elseif ($this->isOfficeDocument($sFileName) || $sFileName === 'diff.zip' || $sFileName === 'changes.json') {
236: if ($this->isTrustedRequest()) {
237: $sAuthToken = isset($aValues['AuthToken']) ? $aValues['AuthToken'] : null;
238: if (isset($sAuthToken)) {
239: Api::setAuthToken($sAuthToken);
240: Api::setUserId(
241: Api::getAuthenticatedUserId($sAuthToken)
242: );
243: }
244: }
245: }
246: }
247: }
248:
249: public function EntryEditor()
250: {
251: $sResult = '';
252: $sFullUrl = Application::getBaseUrl();
253: $sMode = 'view';
254: $fileuri = isset($_GET['editor']) ? $_GET['editor'] : null;
255: $filename = null;
256: $sHash = null;
257: $aHashValues = [];
258: $docKey = null;
259: $lastModified = time();
260: $aHistory = [];
261:
262: $oUser = Api::getAuthenticatedUser();
263:
264: if (isset($fileuri)) {
265: $fileuri = \urldecode($fileuri);
266: $aFileuri = \explode('/', $fileuri);
267: if (isset($aFileuri[1])) {
268: $sHash = $aFileuri[1];
269: $aHashValues = Api::DecodeKeyValues($sHash);
270: if (!isset($aHashValues['AuthToken'])) {
271: $aHashValues['AuthToken'] = Api::UserSession()->Set(
272: [
273: 'token' => 'auth',
274: 'id' => Api::getAuthenticatedUserId()
275: ],
276: time(),
277: time() + 60 * 5 // 5 min
278: );
279: }
280: }
281: $sHash = Api::EncodeKeyValues($aHashValues);
282: $aFileuri[1] = $sHash;
283: $fileuri = implode('/', $aFileuri);
284: $fileuri = $sFullUrl . '?' . $fileuri;
285: }
286:
287: if (isset($sHash)) {
288: $aHashValues = Api::DecodeKeyValues($sHash);
289: if (isset($aHashValues['FileName'])) {
290: $filename = $aHashValues['FileName'];
291: } elseif (isset($aHashValues['Name'])) {
292: $filename = $aHashValues['Name'];
293: }
294: if (isset($aHashValues['Edit'])) {
295: $sMode = 'edit';
296: }
297:
298: $sHash = Api::EncodeKeyValues($aHashValues);
299: $oFileInfo = null;
300: try {
301: if (isset($aHashValues['UserId'], $aHashValues['Type'], $aHashValues['Path'], $aHashValues['Id'])) {
302: $oFileInfo = FilesModule::Decorator()->GetFileInfo(
303: $aHashValues['UserId'],
304: $aHashValues['Type'],
305: $aHashValues['Path'],
306: $aHashValues['Id']
307: );
308: }
309: } catch (\Exception $oEx) {
310: }
311:
312: if ($oFileInfo) {
313: $lastModified = $oFileInfo->LastModified;
314: $docKey = \md5($oFileInfo->RealPath . $lastModified);
315: $oFileInfo->Path = $aHashValues['Path'];
316: $SharedWithMeAccess = isset($oFileInfo->ExtendedProps['SharedWithMeAccess']) ? (int) $oFileInfo->ExtendedProps['SharedWithMeAccess'] : null;
317:
318: if (!isset($SharedWithMeAccess) && $oFileInfo->Owner !== $oUser->PublicId) {
319: list($sParentPath, $sParentId) = \Sabre\Uri\split($aHashValues['Path']);
320: $oParentFileInfo = null;
321: try {
322: $oParentFileInfo = FilesModule::Decorator()->GetFileInfo(
323: $aHashValues['UserId'],
324: $aHashValues['Type'],
325: $sParentPath,
326: $sParentId
327: );
328: } catch (\Exception $oEx) {
329: }
330:
331: if (isset($oParentFileInfo) && $oParentFileInfo->Owner === $oUser->PublicId) {
332: $oFileInfo->Owner = $oParentFileInfo->Owner;
333: }
334: }
335:
336: $sMode = (isset($SharedWithMeAccess) && ($SharedWithMeAccess === Permission::Write || $SharedWithMeAccess === Permission::Reshare)) ||
337: (!isset($SharedWithMeAccess) && $oFileInfo->Owner === $oUser->PublicId) || ($oFileInfo->TypeStr === FileStorageType::Corporate) ? $sMode : 'view';
338: $aHistory = $this->getHistory($oFileInfo, $docKey, $fileuri);
339: } elseif (isset($aHashValues['FileName'])) {
340: $docKey = \md5($aHashValues['FileName'] . time());
341: } elseif (isset($aHashValues['Name'])) {
342: $docKey = \md5($aHashValues['Name'] . time());
343: }
344: }
345:
346: $bIsReadOnlyMode = ($sMode === 'view') ? true : false;
347:
348: $filetype = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
349: $lang = 'en';
350: $mode = $bIsReadOnlyMode || $this->isReadOnlyDocument($filename) ? 'view' : 'edit';
351: $fileuriUser = '';
352:
353: $serverPath = $this->getConfig('DocumentServerUrl', null);
354:
355: $callbackUrl = $sFullUrl . '?ode-callback/' . $sHash;
356:
357: if (isset($fileuri) && isset($serverPath)) {
358: if ($oUser) {
359: $uid = (string) $oUser->Id;
360: $uname = !empty($oUser->Name) ? $oUser->Name : $oUser->PublicId;
361: $lang = \Aurora\System\Utils::ConvertLanguageNameToShort($oUser->Language);
362: }
363:
364: $config = [
365: "type" => Api::IsMobileApplication() ? "mobile" : 'desktop',
366: "documentType" => $this->getDocumentType($filename),
367: "document" => [
368: "title" => $filename,
369: "url" => $fileuri,
370: "fileType" => $filetype,
371: "key" => $docKey,
372: "info" => [
373: "owner" => $uname,
374: "uploaded" => date('d.m.y', $lastModified)
375: ],
376: "permissions" => [
377: "comment" => !$bIsReadOnlyMode,
378: "download" => true,
379: "edit" => !$bIsReadOnlyMode,
380: "fillForms" => !$bIsReadOnlyMode,
381: "modifyFilter" => !$bIsReadOnlyMode,
382: "modifyContentControl" => !$bIsReadOnlyMode,
383: "review" => !$bIsReadOnlyMode,
384: "changeHistory" => !$bIsReadOnlyMode,
385: "chat" => !$bIsReadOnlyMode
386: ]
387: ],
388: "editorConfig" => [
389: "actionLink" => empty($_GET["actionLink"]) ? null : json_decode($_GET["actionLink"]),
390: "mode" => $mode,
391: "lang" => $lang,
392: "callbackUrl" => $callbackUrl,
393: "user" => [
394: "id" => $uid,
395: "name" => $uname
396: ],
397: "embedded" => [
398: "saveUrl" => $fileuriUser,
399: "embedUrl" => $fileuriUser,
400: "shareUrl" => $fileuriUser,
401: "toolbarDocked" => "top",
402: ],
403: "customization" => [
404: "comments" => !$bIsReadOnlyMode,
405: "about" => false,
406: "feedback" => false,
407: "goback" => false,
408: "forcesave" => true,
409: // "logo"=> [
410: // "image"=> $sFullUrl . 'static/styles/images/logo.png',
411: // ],
412: ]
413: ]
414: ];
415:
416: $oJwt = new Classes\JwtManager($this->getConfig('Secret', ''));
417: if ($oJwt->isJwtEnabled()) {
418: $config['token'] = $oJwt->jwtEncode($config);
419: }
420:
421: $sResult = \file_get_contents($this->GetPath().'/templates/Editor.html');
422:
423: $iUserId = Api::getAuthenticatedUserId();
424: if (0 < $iUserId) {
425: $sResult = strtr($sResult, [
426: '{{DOC_SERV_API_URL}}' => $serverPath . '/web-apps/apps/api/documents/api.js',
427: '{{CONFIG}}' => \json_encode($config),
428: '{{HISTORY}}' => isset($aHistory[0]) ? \json_encode($aHistory[0]) : 'false',
429: '{{HISTORY_DATA}}' => isset($aHistory[1]) ? \json_encode($aHistory[1]) : 'false'
430: ]);
431: \Aurora\Modules\CoreWebclient\Module::Decorator()->SetHtmlOutputHeaders();
432: @header('Cache-Control: no-cache, no-store, must-revalidate', true);
433: @header('Pragma: no-cache', true);
434: @header('Expires: 0', true);
435: } else {
436: Api::Location('./');
437: }
438: }
439:
440: return $sResult;
441: }
442:
443: public function CreateBlankDocument($Type, $Path, $FileName)
444: {
445: $mResult = false;
446: $ext = strtolower(pathinfo($FileName, PATHINFO_EXTENSION));
447: $sFilePath = $this->GetPath() . "/data/new." . $ext;
448: if (file_exists($sFilePath)) {
449: $rData = \fopen($sFilePath, "r");
450: $FileName = FilesModule::Decorator()->GetNonExistentFileName(
451: Api::getAuthenticatedUserId(),
452: $Type,
453: $Path,
454: $FileName
455: );
456: $mResult = $this->createFile(
457: Api::getAuthenticatedUserId(),
458: $Type,
459: $Path,
460: $FileName,
461: $rData
462: );
463: \fclose($rData);
464:
465: if ($mResult) {
466: $mResult = FilesModule::Decorator()->GetFileInfo(
467: Api::getAuthenticatedUserId(),
468: $Type,
469: $Path,
470: $FileName
471: );
472: }
473: }
474: return $mResult;
475: }
476:
477: public function ConvertDocument($Type, $Path, $FileName)
478: {
479: $mResult = false;
480: $aExtensions = $this->getExtensionsToConvert();
481: $sExtension = pathinfo($FileName, PATHINFO_EXTENSION);
482: $sNewExtension = isset($aExtensions[$sExtension]) ? $aExtensions[$sExtension] : null;
483: if ($sNewExtension === null) {
484: throw new Exceptions\Exception(Enums\ErrorCodes::ExtensionCannotBeConverted);
485: } else {
486: $mResult = self::Decorator()->ConvertDocumentToFormat($Type, $Path, $FileName, $sNewExtension);
487: }
488:
489: return $mResult;
490: }
491:
492: public function ConvertDocumentToFormat($Type, $Path, $FileName, $ToExtension)
493: {
494: $mResult = false;
495: $oFileInfo = FilesModule::Decorator()->GetFileInfo(
496: Api::getAuthenticatedUserId(),
497: $Type,
498: $Path,
499: $FileName
500: );
501: if ($oFileInfo instanceof FileItem) {
502: $sConvertedDocumentUri = null;
503: $aPathParts = pathinfo($FileName);
504: $sFromExtension = $aPathParts['extension'];
505: $sFileNameWOExt = $aPathParts['filename'];
506: $sDocumentUri = '';
507:
508: if (isset($oFileInfo->Actions['download']['url'])) {
509: $sDownloadUrl = $oFileInfo->Actions['download']['url'];
510: $aUrlParts = \explode('/', $sDownloadUrl);
511: if (isset($aUrlParts[1])) {
512: $aUrlParts[1] = $this->GetFileTempHash($aUrlParts[1]);
513: $sDocumentUri = Application::getBaseUrl() . \implode('/', $aUrlParts);
514: $this->GetConvertedUri(
515: $sDocumentUri,
516: $sFromExtension,
517: $ToExtension,
518: '',
519: false,
520: $sConvertedDocumentUri
521: );
522: if (!empty($sConvertedDocumentUri)) {
523: $rData = \file_get_contents($sConvertedDocumentUri);
524: if ($rData !== false) {
525: $sNewFileName = FilesModule::Decorator()->GetNonExistentFileName(
526: Api::getAuthenticatedUserId(),
527: $Type,
528: $Path,
529: $sFileNameWOExt . '.' . $ToExtension
530: );
531: $mResult = $this->createFile(
532: Api::getAuthenticatedUserId(),
533: $Type,
534: $Path,
535: $sNewFileName,
536: $rData
537: );
538: if ($mResult) {
539: $mResult = FilesModule::Decorator()->GetFileInfo(
540: Api::getAuthenticatedUserId(),
541: $Type,
542: $Path,
543: $sNewFileName
544: );
545: }
546: }
547: }
548: }
549: }
550: }
551:
552: return $mResult;
553: }
554:
555: protected function GetFileTempHash($sHash)
556: {
557: $aValues = Api::DecodeKeyValues($sHash);
558:
559: $sFileName = isset($aValues['FileName']) ? urldecode($aValues['FileName']) : '';
560: if (empty($sFileName)) {
561: $sFileName = isset($aValues['Name']) ? urldecode($aValues['Name']) : '';
562: }
563: $aValues['AuthToken'] = Api::UserSession()->Set(
564: [
565: 'token' => 'auth',
566: 'id' => Api::getAuthenticatedUserId()
567: ],
568: time(),
569: time() + 60 * 5 // 5 min
570: );
571:
572: return Api::EncodeKeyValues($aValues);
573: }
574:
575: protected function GetConvertedUri($document_uri, $from_extension, $to_extension, $document_revision_id, $is_async, &$converted_document_uri)
576: {
577: $converted_document_uri = "";
578: $responceFromConvertService = $this->SendRequestToConvertService(
579: $document_uri,
580: $from_extension,
581: $to_extension,
582: $document_revision_id,
583: $is_async
584: );
585: $json = \json_decode($responceFromConvertService, true);
586:
587: $errorElement = isset($json["error"]) ? $json["error"] : null;
588: if ($errorElement != null && $errorElement != "") {
589: $this->ProcessConvServResponceError($errorElement);
590: }
591:
592: $isEndConvert = $json["endConvert"];
593: $percent = $json["percent"];
594:
595: if ($isEndConvert != null && $isEndConvert == true) {
596: $converted_document_uri = $json["fileUrl"];
597: $percent = 100;
598: } elseif ($percent >= 100) {
599: $percent = 99;
600: }
601:
602: return $percent;
603: }
604:
605: protected function SendRequestToConvertService($document_uri, $from_extension, $to_extension, $document_revision_id, $is_async)
606: {
607: $title = basename($document_uri);
608: if (empty($title)) {
609: $title = \Sabre\DAV\UUIDUtil::getUUID();
610: }
611:
612: if (empty($document_revision_id)) {
613: $document_revision_id = $document_uri;
614: }
615:
616: $document_revision_id = $this->GenerateRevisionId($document_revision_id);
617:
618: $serverPath = $this->getConfig('DocumentServerUrl', null);
619: if ($serverPath !== null) {
620: $urlToConverter = $serverPath . '/ConvertService.ashx';
621: }
622:
623: $arr = [
624: "async" => $is_async,
625: "url" => $document_uri,
626: "outputtype" => trim($to_extension, '.'),
627: "filetype" => trim($from_extension, '.'),
628: "title" => $title,
629: "key" => $document_revision_id
630: ];
631:
632: $headerToken = "";
633:
634: $oJwt = new Classes\JwtManager($this->getConfig('Secret', ''));
635: if ($oJwt->isJwtEnabled()) {
636: $headerToken = $oJwt->jwtEncode([ "payload" => $arr ]);
637: $arr["token"] = $oJwt->jwtEncode($arr);
638: }
639:
640: $opts = [
641: 'http' => [
642: 'method' => 'POST',
643: 'timeout' => '120000',
644: 'header'=> "Content-type: application/json\r\n" .
645: "Accept: application/json\r\n" .
646: (empty($headerToken) ? "" : "Authorization: $headerToken\r\n"),
647: 'content' => \json_encode($arr)
648: ]
649: ];
650:
651: if (substr($urlToConverter, 0, strlen("https")) === "https") {
652: $opts['ssl'] = ['verify_peer' => false];
653: }
654:
655: $context = stream_context_create($opts);
656: $response_data = file_get_contents($urlToConverter, false, $context);
657:
658: return $response_data;
659: }
660:
661: protected function ProcessConvServResponceError($errorCode)
662: {
663: $errorMessageTemplate = "Error occurred in the document service: ";
664: $errorMessage = '';
665:
666: switch ($errorCode) {
667: case -8:
668: $errorMessage = $errorMessageTemplate . "Error document VKey";
669: break;
670: case -7:
671: $errorMessage = $errorMessageTemplate . "Error document request";
672: break;
673: case -6:
674: $errorMessage = $errorMessageTemplate . "Error database";
675: break;
676: case -5:
677: $errorMessage = $errorMessageTemplate . "Error unexpected guid";
678: break;
679: case -4:
680: $errorMessage = $errorMessageTemplate . "Error download error";
681: break;
682: case -3:
683: $errorMessage = $errorMessageTemplate . "Error convertation error";
684: break;
685: case -2:
686: $errorMessage = $errorMessageTemplate . "Error convertation timeout";
687: break;
688: case -1:
689: $errorMessage = $errorMessageTemplate . "Error convertation unknown";
690: break;
691: case 0:
692: break;
693: default:
694: $errorMessage = $errorMessageTemplate . "ErrorCode = " . $errorCode;
695: break;
696: }
697:
698: throw new Exception($errorMessage);
699: }
700:
701: protected function GenerateRevisionId($expected_key)
702: {
703: if (strlen($expected_key) > 20) {
704: $expected_key = crc32($expected_key);
705: }
706: $key = preg_replace("[^0-9-.a-zA-Z_=]", "_", $expected_key);
707: $key = substr($key, 0, min(array(strlen($key), 20)));
708: return $key;
709: }
710:
711: public function EntryCallback()
712: {
713: $result = ["error" => 0];
714:
715: if (($body_stream = file_get_contents("php://input")) === false) {
716: $result["error"] = "Bad Request";
717: } else {
718: $data = json_decode($body_stream, true);
719:
720: if (isset($data['token'])) {
721: Api::AddSecret($data['token']);
722: }
723: Api::Log($body_stream);
724:
725: $oJwt = new Classes\JwtManager($this->getConfig('Secret', ''));
726: if ($oJwt->isJwtEnabled()) {
727: $inHeader = false;
728: $token = "";
729: if (!empty($data["token"])) {
730: $token = $oJwt->jwtDecode($data["token"]);
731: } elseif (!empty($_SERVER['HTTP_AUTHORIZATION'])) {
732: $token = $oJwt->jwtDecode(substr($_SERVER['HTTP_AUTHORIZATION'], strlen("Bearer ")));
733: $inHeader = true;
734: } else {
735: $result["error"] = "Expected JWT";
736: }
737: if (empty($token)) {
738: $result["error"] = "Invalid JWT signature";
739: } else {
740: $data = json_decode($token, true);
741: if ($inHeader) {
742: $data = $data["payload"];
743: }
744: }
745: }
746:
747: if ($data["status"] == 2 || $data["status"] == 6) {
748: $sHash = (string) \Aurora\System\Router::getItemByIndex(1, '');
749: if (!empty($sHash)) {
750: $aHashValues = Api::DecodeKeyValues($sHash);
751:
752: $prevState = Api::skipCheckUserRole(true);
753: $oFileInfo = FilesModule::Decorator()->GetFileInfo(
754: $aHashValues['UserId'],
755: $aHashValues['Type'],
756: $aHashValues['Path'],
757: $aHashValues['Name']
758: );
759: if ($oFileInfo instanceof FileItem && $this->isOfficeDocument($oFileInfo->Name)) {
760: if ((isset($oFileInfo->ExtendedProps['SharedWithMeAccess']) && (int) $oFileInfo->ExtendedProps['SharedWithMeAccess'] === \Afterlogic\DAV\FS\Permission::Write) || !isset($oFileInfo->ExtendedProps['SharedWithMeAccess'])
761: && !$this->isReadOnlyDocument($oFileInfo->Name)) {
762: $rData = \file_get_contents($data["url"]);
763: if ($rData !== false) {
764: if ($this->getConfig('EnableHistory', false) && $data["status"] == 2) {
765: if ($this->isTrustedRequest()) {
766: $iUserId = isset($aHashValues['UserId']) ? $aHashValues['UserId'] : null;
767: if (isset($iUserId)) {
768: Server::setUser(Api::getUserPublicIdById($iUserId));
769: }
770: }
771: $histDir = $this->getHistoryDir($oFileInfo, true);
772: if ($histDir) {
773: $curVer = $histDir->getFileVersion();
774: $verDir = $histDir->getVersionDir($curVer + 1, true);
775: $ext = strtolower(pathinfo($oFileInfo->Name, PATHINFO_EXTENSION));
776:
777: list(, $sOwnerUserPublicId) = split($verDir->getOwner());
778: $iOwnerUserId = Api::getUserIdByPublicId($sOwnerUserPublicId);
779:
780: if (!isset($oFileInfo->ExtendedProps['Created']) && $curVer == 0) {
781: FilesModule::Decorator()->UpdateExtendedProps(
782: $iOwnerUserId,
783: $aHashValues['Type'],
784: $oFileInfo->Path,
785: $oFileInfo->Name,
786: [
787: 'Created' => $oFileInfo->LastModified
788: ]
789: );
790: }
791:
792: $fileContent = FilesModule::Decorator()->GetFileContent(
793: $aHashValues['UserId'],
794: $aHashValues['Type'],
795: $aHashValues['Path'],
796: $aHashValues['Name']
797: );
798: $verDir->createFile('prev.' . $ext, $fileContent);
799: $prevChild = $verDir->getChild('prev.' . $ext);
800: if ($prevChild) {
801: $mExtendedProps = $prevChild->getProperty('ExtendedProps');
802: $aExtendedProps = is_array($mExtendedProps) ? $mExtendedProps : [];
803: $aExtendedProps['Created'] = $oFileInfo->LastModified;
804:
805: $prevChild->setProperty('ExtendedProps', $aExtendedProps);
806: }
807: $this->updateHistory($verDir, $data);
808: }
809: }
810:
811: $this->createFile(
812: $aHashValues['UserId'],
813: $aHashValues['Type'],
814: $aHashValues['Path'],
815: $aHashValues['Name'],
816: $rData
817: );
818: }
819: }
820: }
821: Api::skipCheckUserRole($prevState);
822: }
823: }
824: }
825: return json_encode($result);
826: }
827:
828: /**
829: *
830: * @param type $aArguments
831: * @param type $aResult
832: */
833: public function onGetFile(&$aArguments, &$aResult)
834: {
835: if ($this->isOfficeDocument($aArguments['Name'])) {
836: $aArguments['NoRedirect'] = true;
837: }
838: }
839:
840: /**
841: * Writes to $aData variable list of DropBox files if $aData['Type'] is DropBox account type.
842: *
843: * @ignore
844: * @param array $aData Is passed by reference.
845: */
846: public function onGetItems($aArgs, &$mResult)
847: {
848: if (is_array($mResult)) {
849: foreach ($mResult as $oItem) {
850: if ($oItem instanceof FileItem && $this->isOfficeDocument($oItem->Name)) {
851: $bEncrypted = isset($oItem->ExtendedProps['InitializationVector']);
852: $bAccessSet = isset($oItem->ExtendedProps['SharedWithMeAccess']);
853: $bHasWriteAccess = !$bAccessSet || ($bAccessSet && ((int) $oItem->ExtendedProps['SharedWithMeAccess'] === \Afterlogic\DAV\FS\Permission::Write || (int) $oItem->ExtendedProps['SharedWithMeAccess'] === \Afterlogic\DAV\FS\Permission::Reshare));
854: if (!$bEncrypted && $bHasWriteAccess) {
855: if ($this->documentCanBeConverted($oItem->Name)) {
856: $oItem->UnshiftAction([
857: 'convert' => [
858: 'url' => ''
859: ]
860: ]);
861: } elseif ($this->documentCanBeEdited($oItem->Name)) {
862: $sHash = $oItem->getHash();
863: $aHashValues = Api::DecodeKeyValues($sHash);
864: $aHashValues['Edit'] = true;
865: $sHash = Api::EncodeKeyValues($aHashValues);
866: $oItem->UnshiftAction([
867: 'edit' => [
868: 'url' => '?download-file/' . $sHash .'/view'
869: ]
870: ]);
871: }
872: }
873: }
874: }
875: }
876: }
877:
878: public function onAfterGetFileInfo($aArgs, &$mResult)
879: {
880: if ($mResult) {
881: if ($mResult instanceof FileItem && $this->isOfficeDocument($mResult->Name)) {
882: if ((isset($mResult->ExtendedProps['SharedWithMeAccess']) && ((int) $mResult->ExtendedProps['SharedWithMeAccess'] === \Afterlogic\DAV\FS\Permission::Write || (int) $mResult->ExtendedProps['SharedWithMeAccess'] === \Afterlogic\DAV\FS\Permission::Reshare)) || !isset($mResult->ExtendedProps['SharedWithMeAccess'])
883: && !$this->isReadOnlyDocument($mResult->Name)) {
884: $sHash = $mResult->getHash();
885: $aHashValues = Api::DecodeKeyValues($sHash);
886: $aHashValues['Edit'] = true;
887: $sHash = Api::EncodeKeyValues($aHashValues);
888: $mResult->UnshiftAction([
889: 'edit' => [
890: 'url' => '?download-file/' . $sHash .'/view'
891: ]
892: ]);
893: }
894: }
895: }
896: }
897:
898: public function onAddToContentSecurityPolicyDefault($aArgs, &$aAddDefault)
899: {
900: $sUrl = $this->getConfig('DocumentServerUrl', null);
901: if (!empty($sUrl)) {
902: $aAddDefault[] = $sUrl;
903: }
904: }
905:
906:
907: // History
908: public function RestoreFromHistory($Url, $Version)
909: {
910: }
911:
912: protected function getHistoryDir($oFileInfo, $bCreateIfNotExists = false)
913: {
914: $oHistNode = false;
915:
916: $sType = $oFileInfo->TypeStr;
917: $sPath = $oFileInfo->Path;
918: $sName = $oFileInfo->Name;
919: $sOwner = $oFileInfo->Owner;
920:
921: /**
922: * @var $oNode
923: */
924: $oNode = Server::getNodeForPath('files/' . $sType . $sPath . '/' . $sName, $sOwner);
925: if ($oNode instanceof File) {
926: $oHistNode = $oNode->getHistoryDirectory();
927: if (!$oHistNode && $bCreateIfNotExists) {
928: $oParentNode = Server::getNodeForPath('files/' . $sType . $sPath, $sOwner);
929: if ($oParentNode instanceof Directory) {
930: $oParentNode->createDirectory($sName . '.hist');
931: }
932: $oHistNode = $oNode->getHistoryDirectory();
933: }
934: }
935:
936: return $oHistNode;
937: }
938:
939: protected function getHistory($oFileInfo, $docKey, $sUrl)
940: {
941: $result = [];
942: $curVer = 0;
943: $histDir = $this->getHistoryDir($oFileInfo);
944: if ($histDir && method_exists($histDir, 'getFileVersion')) {
945: $curVer = $histDir->getFileVersion();
946: }
947:
948: if ($curVer > 0) {
949: $hist = [];
950: $histData = [];
951: $oUser = CoreModule::getInstance()->GetUserByPublicId($oFileInfo->Owner);
952:
953: for ($i = 0; $i <= $curVer; $i++) {
954: $obj = [];
955: $dataObj = [];
956:
957: $verDir = $histDir->getVersionDir($i + 1);
958:
959: $key = false;
960:
961: if ($i == $curVer) {
962: $key = $docKey;
963: } else {
964: if ($verDir && $verDir->childExists('key.txt')) {
965: $oKeyFile = $verDir->getChild('key.txt');
966: if ($oKeyFile instanceof \Afterlogic\DAV\FS\File) {
967: $mKeyData = $oKeyFile->get(false);
968: if (is_resource($mKeyData)) {
969: $key = \stream_get_contents($mKeyData);
970: }
971: }
972: } else {
973: $key = false;
974: }
975: }
976:
977: if ($key === false) {
978: continue;
979: }
980: $obj["key"] = $key;
981: $obj["version"] = $i + 1;
982:
983: if ($i === 0) {
984: $ext = strtolower(pathinfo($oFileInfo->Name, PATHINFO_EXTENSION));
985: list(, $sUserPublicId) = split($verDir->getOwner());
986:
987: $prevDoc = $verDir->getChild('prev.' . $ext);
988: $aPrevFileExtendedProps = [];
989: if ($prevDoc) {
990: $aPrevFileExtendedProps = $prevDoc->getProperty('ExtendedProps');
991: }
992:
993: if (isset($aPrevFileExtendedProps['Created'])) {
994: $obj["created"] = $this->convetToUserTime(
995: $oUser,
996: date("Y-m-d H:i:s", $aPrevFileExtendedProps['Created'])
997: );
998: } else {
999: $obj["created"] = '';
1000: }
1001: $obj["user"] = [
1002: "id" => (string) $oUser->Id,
1003: "name" => $oFileInfo->Owner
1004: ];
1005: }
1006:
1007: if ($i != $curVer) {
1008: $ext = strtolower(pathinfo($oFileInfo->Name, PATHINFO_EXTENSION));
1009:
1010: $sUrl = $this->getDownloadUrl(
1011: //Api::getUserIdByPublicId($sUserPublicId),
1012: Api::getUserIdByPublicId($verDir->getUser()),
1013: // $oFileInfo->TypeStr,
1014: $verDir->getStorage(),
1015: $verDir->getRelativePath() . '/' . $verDir->getName(),
1016: 'prev.' . $ext
1017: );
1018: }
1019:
1020: $dataObj["key"] = $key;
1021: $dataObj["url"] = $sUrl;
1022: $dataObj["version"] = $i + 1;
1023:
1024: if ($i > 0) {
1025: $changes = false;
1026: $verDirPrev = $histDir->getVersionDir($i);
1027: if ($verDirPrev && $verDirPrev->childExists('changes.json')) {
1028: $oChangesFile = $verDirPrev->getChild('changes.json');
1029: if ($oChangesFile instanceof \Afterlogic\DAV\FS\File) {
1030: $mChangesFileData = $oChangesFile->get(false);
1031: if (is_resource($mChangesFileData)) {
1032: $changes = \json_decode(\stream_get_contents($mChangesFileData), true);
1033: }
1034: }
1035: }
1036:
1037: if (!$changes) {
1038: continue;
1039: }
1040: $change = $changes["changes"][0];
1041:
1042: $obj["changes"] = $changes["changes"];
1043: $obj["serverVersion"] = $changes["serverVersion"];
1044: $obj["created"] = $this->convetToUserTime(
1045: $oUser,
1046: $change["created"]
1047: );
1048: $obj["user"] = $change["user"];
1049:
1050: if (isset($histData[$i])) {
1051: $prev = $histData[$i];
1052: $dataObj["previous"] = [
1053: "key" => $prev["key"],
1054: "url" => $prev["url"]
1055: ];
1056: }
1057:
1058: $dataObj["changesUrl"] = $this->getDownloadUrl(
1059: //Api::getUserIdByPublicId($sUserPublicId),
1060: Api::getUserIdByPublicId($histDir->getUser()),
1061: //$oFileInfo->TypeStr,
1062: $histDir->getStorage(),
1063: $histDir->getVersionDir($i)->getRelativePath() . '/' . $verDirPrev->getName(),
1064: 'diff.zip'
1065: );
1066: }
1067:
1068: array_push($hist, $obj);
1069: $oJwt = new Classes\JwtManager($this->getConfig('Secret', ''));
1070: if ($oJwt->isJwtEnabled()) {
1071: $dataObj['token'] = $oJwt->jwtEncode($dataObj);
1072: }
1073: $histData[$i + 1] = $dataObj;
1074: }
1075:
1076: array_push(
1077: $result,
1078: [
1079: "currentVersion" => $curVer + 1,
1080: "history" => $hist
1081: ],
1082: $histData
1083: );
1084: }
1085:
1086: return $result;
1087: }
1088:
1089: protected function updateHistory($verDir, $data)
1090: {
1091: if (isset($data["changesurl"])) {
1092: $rChangesData = \file_get_contents($data["changesurl"]);
1093: if ($rChangesData !== false) {
1094: $verDir->createFile('diff.zip', $rChangesData);
1095: }
1096: }
1097: $histData = isset($data["changeshistory"]) ? $data["changeshistory"] : '';
1098: if (empty($histData)) {
1099: $histData = json_encode($data["history"], JSON_PRETTY_PRINT);
1100: }
1101: if (!empty($histData)) {
1102: $verDir->createFile('changes.json', $histData);
1103: }
1104: $verDir->createFile('key.txt', $data['key']);
1105: }
1106:
1107: protected function createFile($iUserId, $sType, $sPath, $sFileName, $mData)
1108: {
1109: Api::Log(self::GetName() . '::writeFile');
1110:
1111: $mResult = false;
1112: $aArgs = [
1113: 'UserId' => $iUserId,
1114: 'Type' => $sType,
1115: 'Path' => $sPath,
1116: 'Name' => $sFileName,
1117: 'Data' => $mData,
1118: 'Overwrite' => true,
1119: 'RangeType' => 0,
1120: 'Offset' => 0,
1121: 'ExtendedProps' => []
1122: ];
1123:
1124: $this->broadcastEvent(
1125: 'Files::CreateFile',
1126: $aArgs,
1127: $mResult
1128: );
1129:
1130: return $mResult;
1131: }
1132:
1133: protected function getDownloadUrl($iUserId, $sType, $sPath, $sName)
1134: {
1135: $sHash = Api::EncodeKeyValues([
1136: 'UserId' => $iUserId,
1137: 'Id' => $sName,
1138: 'Type' => $sType,
1139: 'Path' => $sPath,
1140: 'Name' => $sName,
1141: 'FileName' => $sName,
1142: 'AuthToken' => Api::UserSession()->Set([
1143: 'token' => 'auth',
1144: 'id' => $iUserId,
1145: 't' => time(),
1146: ])
1147: ]);
1148:
1149: return Application::getBaseUrl() . '?download-file/' . $sHash;
1150: }
1151:
1152: protected function convetToUserTime($oUser, $sTime)
1153: {
1154: $dt = \DateTime::createFromFormat(
1155: 'Y-m-d H:i:s',
1156: $sTime,
1157: new \DateTimeZone('UTC')
1158: );
1159: if (!empty($oUser->DefaultTimeZone)) {
1160: $dt->setTimezone(new \DateTimeZone($oUser->DefaultTimeZone));
1161: }
1162:
1163: return $dt->format("Y-m-d H:i:s");
1164: }
1165: }
1166: