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