1: | <?php |
2: | |
3: | |
4: | |
5: | |
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: | |
26: | |
27: | |
28: | |
29: | |
30: | |
31: | |
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: | |
86: | |
87: | |
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: | |
109: | |
110: | public static function getInstance() |
111: | { |
112: | return parent::getInstance(); |
113: | } |
114: | |
115: | |
116: | |
117: | |
118: | public static function Decorator() |
119: | { |
120: | return parent::Decorator(); |
121: | } |
122: | |
123: | |
124: | |
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: | |
203: | |
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; |
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: | |
238: | |
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 |
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: | |
436: | |
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 |
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: | |
859: | |
860: | |
861: | public function onGetFile(&$aArguments, &$aResult) |
862: | { |
863: | if ($this->isOfficeDocument($aArguments['Name'])) { |
864: | $aArguments['NoRedirect'] = true; |
865: | } |
866: | } |
867: | |
868: | |
869: | |
870: | |
871: | |
872: | |
873: | |
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: | |
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: | |
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: | |
1039: | Api::getUserIdByPublicId($verDir->getUser()), |
1040: | |
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: | |
1087: | Api::getUserIdByPublicId($histDir->getUser()), |
1088: | |
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: | |