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\DropboxFilestorage;
9:
10: /**
11: * Adds ability to work with Dropbox file storage inside Aurora Files module.
12: *
13: * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
14: * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
15: * @copyright Copyright (c) 2023, Afterlogic Corp.
16: *
17: * @package Modules
18: */
19: class Module extends \Aurora\System\Module\AbstractModule
20: {
21: protected static $sStorageType = 'dropbox';
22: protected static $iStorageOrder = 100;
23: protected $oClient = null;
24: protected $aRequireModules = array(
25: 'OAuthIntegratorWebclient',
26: 'DropboxAuthWebclient'
27: );
28:
29: protected function issetScope($sScope)
30: {
31: return in_array($sScope, explode(' ', $this->getConfig('Scopes')));
32: }
33:
34: /***** private functions *****/
35: /**
36: * Initializes DropBox Module.
37: *
38: * @ignore
39: */
40: public function init()
41: {
42: $this->subscribeEvent('Files::GetStorages::after', array($this, 'onAfterGetStorages'));
43: $this->subscribeEvent('Files::GetFile', array($this, 'onGetFile'));
44: $this->subscribeEvent('Files::GetItems', array($this, 'onGetItems'));
45: $this->subscribeEvent('Files::CreateFolder::after', array($this, 'onAfterCreateFolder'));
46: $this->subscribeEvent('Files::CreateFile', array($this, 'onCreateFile'));
47: $this->subscribeEvent('Files::Delete::after', array($this, 'onAfterDelete'));
48: $this->subscribeEvent('Files::Rename::after', array($this, 'onAfterRename'));
49: $this->subscribeEvent('Files::Move::after', array($this, 'onAfterMove'));
50: $this->subscribeEvent('Files::Copy::after', array($this, 'onAfterCopy'));
51: $this->subscribeEvent('Files::GetFileInfo::after', array($this, 'onAfterGetFileInfo'));
52: $this->subscribeEvent('Files::PopulateFileItem::after', array($this, 'onAfterPopulateFileItem'));
53:
54: $this->subscribeEvent('Dropbox::GetSettings', array($this, 'onGetSettings'));
55: $this->subscribeEvent('Dropbox::UpdateSettings::after', array($this, 'onAfterUpdateSettings'));
56:
57: $this->subscribeEvent('Files::GetItems::before', array($this, 'CheckUrlFile'));
58: $this->subscribeEvent('Files::UploadFile::before', array($this, 'CheckUrlFile'));
59: $this->subscribeEvent('Files::CreateFolder::before', array($this, 'CheckUrlFile'));
60: $this->subscribeEvent('Files::CheckQuota::after', array($this, 'onAfterCheckQuota'));
61: }
62:
63: /**
64: * Obtains DropBox client if passed $sType is DropBox account type.
65: *
66: * @param string $sType Service type.
67: * @return \Dropbox\Client
68: */
69: protected function getClient()
70: {
71: $oDropboxModule = \Aurora\System\Api::GetModule('Dropbox');
72: if ($oDropboxModule instanceof \Aurora\System\Module\AbstractModule) {
73: if (!$oDropboxModule->getConfig('EnableModule', false) || !$this->issetScope('storage')) {
74: return false;
75: }
76: } else {
77: return false;
78: }
79:
80: if ($this->oClient === null) {
81: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
82:
83: $oOAuthIntegratorWebclientModule = \Aurora\Modules\OAuthIntegratorWebclient\Module::Decorator();
84: $oOAuthAccount = $oOAuthIntegratorWebclientModule->GetAccount(self::$sStorageType);
85: if ($oOAuthAccount) {
86: $oDropboxApp = new \Kunnu\Dropbox\DropboxApp(
87: \Aurora\System\Api::GetModule('Dropbox')->getConfig('Id'),
88: \Aurora\System\Api::GetModule('Dropbox')->getConfig('Secret'),
89: $oOAuthAccount->AccessToken
90: );
91: $this->oClient = new \Kunnu\Dropbox\Dropbox($oDropboxApp);
92: }
93: }
94:
95: return $this->oClient;
96: }
97:
98: /**
99: * Write to the $aResult variable information about DropBox storage.
100: *
101: * @ignore
102: * @param array $aData Is passed by reference.
103: */
104: public function onAfterGetStorages($aArgs, &$mResult)
105: {
106: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
107:
108: $bEnableDropboxModule = false;
109: $oDropboxModule = \Aurora\System\Api::GetModule('Dropbox');
110: if ($oDropboxModule instanceof \Aurora\System\Module\AbstractModule) {
111: $bEnableDropboxModule = $oDropboxModule->getConfig('EnableModule', false);
112: } else {
113: $bEnableDropboxModule = false;
114: }
115:
116:
117: $oOAuthIntegratorWebclientModule = \Aurora\Modules\OAuthIntegratorWebclient\Module::Decorator();
118: $oOAuthAccount = $oOAuthIntegratorWebclientModule->GetAccount(self::$sStorageType);
119:
120: if ($oOAuthAccount instanceof \Aurora\Modules\OAuthIntegratorWebclient\Models\OauthAccount &&
121: $oOAuthAccount->Type === self::$sStorageType &&
122: $this->issetScope('storage') && $oOAuthAccount->issetScope('storage')) {
123: $mResult[] = [
124: 'Type' => self::$sStorageType,
125: 'IsExternal' => true,
126: 'DisplayName' => 'Dropbox',
127: 'Order' => self::$iStorageOrder,
128: 'IsDroppable' => true
129: ];
130: }
131: }
132:
133: /**
134: * Returns directory name for the specified path.
135: *
136: * @param string $sPath Path to the file.
137: * @return string
138: */
139: protected function getDirName($sPath)
140: {
141: $sPath = dirname($sPath);
142: return str_replace(DIRECTORY_SEPARATOR, '/', $sPath);
143: }
144:
145: /**
146: * Returns base name for the specified path.
147: *
148: * @param string $sPath Path to the file.
149: * @return string
150: */
151: protected function getBaseName($sPath)
152: {
153: $aPath = explode('/', $sPath);
154: return end($aPath);
155: }
156:
157: /**
158: *
159: * @param type $oItem
160: * @return type
161: */
162: protected function getItemHash($oItem)
163: {
164: return \Aurora\System\Api::EncodeKeyValues(array(
165: 'UserId' => \Aurora\System\Api::getAuthenticatedUserId(),
166: 'Type' => $oItem->TypeStr,
167: 'Path' => '',
168: 'Name' => $oItem->FullPath,
169: 'FileName' => $oItem->Name
170: ));
171: }
172:
173: protected function hasThumb($sName)
174: {
175: return in_array(
176: pathinfo($sName, PATHINFO_EXTENSION),
177: ['jpg', 'jpeg', 'png', 'tiff', 'tif', 'gif', 'bmp']
178: );
179: }
180:
181: /**
182: * Populates file info.
183: *
184: * @param string $sType Service type.
185: * @param \Dropbox\Client $oClient DropBox client.
186: * @param array $aData Array contains information about file.
187: * @return \Aurora\Modules\Files\Classes\FileItem|false
188: */
189: protected function populateFileInfo($aData)
190: {
191: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
192:
193: $mResult = false;
194: if ($aData) {
195: $sPath = ltrim($this->getDirName($aData->getPathDisplay()), '/');
196:
197: $mResult /*@var $mResult \Aurora\Modules\Files\Classes\FileItem */ = new \Aurora\Modules\Files\Classes\FileItem();
198: $mResult->IsExternal = true;
199: $mResult->TypeStr = self::$sStorageType;
200: $mResult->IsFolder = ($aData instanceof \Kunnu\Dropbox\Models\FolderMetadata);
201: $mResult->Id = $aData->getName();
202: $mResult->Name = $mResult->Id;
203: $mResult->Path = !empty($sPath) ? '/'.$sPath : $sPath;
204: $mResult->Size = !$mResult->IsFolder ? $aData->getSize() : 0;
205: // $bResult->Owner = $oSocial->Name;
206: if (!$mResult->IsFolder) {
207: $mResult->LastModified = date("U", strtotime($aData->getServerModified()));
208: }
209: // $mResult->Shared = isset($aData['shared']) ? $aData['shared'] : false;
210: $mResult->FullPath = $mResult->Name !== '' ? $mResult->Path . '/' . $mResult->Name : $mResult->Path ;
211: $mResult->ContentType = \Aurora\System\Utils::MimeContentType($mResult->Name);
212:
213: $mResult->Thumb = $this->hasThumb($mResult->Name);
214:
215: if ($mResult->IsFolder) {
216: $mResult->AddAction([
217: 'list' => []
218: ]);
219: } else {
220: $mResult->AddAction([
221: 'view' => [
222: 'url' => '?download-file/' . $this->getItemHash($mResult) .'/view'
223: ]
224: ]);
225: $mResult->AddAction([
226: 'download' => [
227: 'url' => '?download-file/' . $this->getItemHash($mResult)
228: ]
229: ]);
230: }
231: }
232: return $mResult;
233: }
234:
235: /**
236: * Writes to the $mResult variable open file source if $sType is DropBox account type.
237: *
238: * @ignore
239: * @param int $iUserId Identifier of the authenticated user.
240: * @param string $sType Service type.
241: * @param string $sFullPath File path.
242: * @param string $sName File name.
243: * @param boolean $bThumb **true** if thumbnail is expected.
244: * @param mixed $mResult
245: */
246: public function onGetFile($aArgs, &$mResult)
247: {
248: if ($aArgs['Type'] === self::$sStorageType) {
249: $oClient = $this->getClient();
250: if ($oClient) {
251: $sFullPath = $aArgs['Path'] . '/' . ltrim($aArgs['Name'], '/');
252:
253: if (isset($aArgs['IsThumb']) && (bool)$aArgs['IsThumb'] === true) {
254: $oThumbnail = $oClient->getThumbnail($sFullPath, 'medium', 'png');
255: if ($oThumbnail) {
256: $mResult = \fopen('php://memory', 'r+');
257: \fwrite($mResult, $oThumbnail->getContents());
258: \rewind($mResult);
259: }
260: } else {
261: $mDownloadResult = $oClient->download($sFullPath);
262: if ($mDownloadResult) {
263: $mResult = \fopen('php://memory', 'r+');
264: \fwrite($mResult, $mDownloadResult->getContents());
265: \rewind($mResult);
266: }
267: }
268: }
269:
270: return true;
271: }
272: }
273:
274: /**
275: * Writes to $aData variable list of DropBox files if $aData['Type'] is DropBox account type.
276: *
277: * @ignore
278: * @param array $aData Is passed by reference.
279: */
280: public function onGetItems($aArgs, &$mResult)
281: {
282: if ($aArgs['Type'] === self::$sStorageType) {
283: $mResult = array();
284: $oClient = $this->getClient();
285: if ($oClient) {
286: $aItems = array();
287: $Path = '/'.ltrim($aArgs['Path'], '/');
288: if (empty($aArgs['Pattern'])) {
289: $oListFolderContents = $oClient->listFolder($Path);
290: $oItems = $oListFolderContents->getItems();
291: $aItems = $oItems->all();
292: } else {
293: $oListFolderContents = $oClient->search($Path, $aArgs['Pattern']);
294: $oItems = $oListFolderContents->getItems();
295: $aItems = $oItems->all();
296: }
297:
298: foreach ($aItems as $oChild) {
299: if ($oChild instanceof \Kunnu\Dropbox\Models\SearchResult) {
300: $oChild = $oChild->getMetadata();
301: }
302: $oItem /*@var $oItem \Aurora\Modules\Files\Classes\FileItem */ = $this->populateFileInfo($oChild);
303: if ($oItem) {
304: $mResult[] = $oItem;
305: }
306: }
307: }
308:
309: return true;
310: }
311: }
312:
313: /**
314: * Creates folder if $aData['Type'] is DropBox account type.
315: *
316: * @ignore
317: * @param array $aData Is passed by reference.
318: */
319: public function onAfterCreateFolder($aArgs, &$mResult)
320: {
321: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
322:
323: if ($aArgs['Type'] === self::$sStorageType) {
324: $oClient = $this->getClient();
325: if ($oClient) {
326: $mResult = false;
327: $sPath = $aArgs['Path'];
328:
329: if ($oClient->createFolder($sPath.'/'.$aArgs['FolderName']) !== null) {
330: $mResult = true;
331: }
332: }
333: return true;
334: }
335: }
336:
337: /**
338: * Creates file if $aData['Type'] is DropBox account type.
339: *
340: * @ignore
341: * @param array $aData
342: */
343: public function onCreateFile($aArgs, &$mResult)
344: {
345: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
346:
347: if ($aArgs['Type'] === self::$sStorageType) {
348: $oClient = $this->getClient();
349: if ($oClient) {
350: $mResult = false;
351:
352: $Path = $aArgs['Path'].'/'.$aArgs['Name'];
353: $rData = $aArgs['Data'];
354: if (!is_resource($aArgs['Data'])) {
355: $rData = fopen('php://memory', 'r+');
356: fwrite($rData, $aArgs['Data']);
357: rewind($rData);
358: }
359: $oDropboxFile = \Kunnu\Dropbox\DropboxFile::createByStream($aArgs['Name'], $rData);
360: if ($oClient->upload($oDropboxFile, $Path)) {
361: $mResult = true;
362: }
363:
364: return true;
365: }
366: }
367: }
368:
369: /**
370: * Deletes file if $aData['Type'] is DropBox account type.
371: *
372: * @ignore
373: * @param array $aData
374: */
375: public function onAfterDelete($aArgs, &$mResult)
376: {
377: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
378:
379: if ($aArgs['Type'] === self::$sStorageType) {
380: $oClient = $this->getClient();
381: if ($oClient) {
382: $mResult = false;
383:
384: foreach ($aArgs['Items'] as $aItem) {
385: $oClient->delete($aItem['Path'].'/'.$aItem['Name']);
386: $mResult = true;
387: }
388: }
389: return true;
390: }
391: }
392:
393: /**
394: * Renames file if $aData['Type'] is DropBox account type.
395: *
396: * @ignore
397: * @param array $aData
398: */
399: public function onAfterRename($aArgs, &$mResult)
400: {
401: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
402:
403: if ($aArgs['Type'] === self::$sStorageType) {
404: $oClient = $this->getClient();
405: if ($oClient) {
406: $mResult = false;
407:
408: $sPath = $aArgs['Path'];
409: if ($oClient->move($sPath.'/'.$aArgs['Name'], $sPath.'/'.$aArgs['NewName'])) {
410: $mResult = true;
411: }
412: }
413: }
414: }
415:
416: /**
417: * Moves file if $aData['Type'] is DropBox account type.
418: *
419: * @ignore
420: * @param array $aData
421: */
422: public function onAfterMove($aArgs, &$mResult)
423: {
424: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
425:
426: if ($aArgs['FromType'] === self::$sStorageType) {
427: $oClient = $this->getClient();
428: if ($oClient) {
429: $mResult = false;
430:
431: if ($aArgs['ToType'] === $aArgs['FromType']) {
432: foreach ($aArgs['Files'] as $aFile) {
433: $oClient->move($aArgs['FromPath'].'/'.$aFile['Name'], $aArgs['ToPath'].'/'.$aFile['Name']);
434: }
435: $mResult = true;
436: }
437: }
438: return true;
439: }
440: }
441:
442: /**
443: * Copies file if $aData['Type'] is DropBox account type.
444: *
445: * @ignore
446: * @param array $aData
447: */
448: public function onAfterCopy($aArgs, &$mResult)
449: {
450: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
451:
452: if ($aArgs['FromType'] === self::$sStorageType) {
453: $oClient = $this->getClient();
454: if ($oClient) {
455: $mResult = false;
456:
457: if ($aArgs['ToType'] === $aArgs['FromType']) {
458: foreach ($aArgs['Files'] as $aFile) {
459: $oClient->copy($aArgs['FromPath'].'/'.$aFile['Name'], $aArgs['ToPath'].'/'.$aFile['Name']);
460: }
461: $mResult = true;
462: }
463: }
464: return true;
465: }
466: }
467:
468: protected function _getFileInfo($sPath, $sName)
469: {
470: $mResult = false;
471: $oClient = $this->GetClient();
472: if ($oClient) {
473: $mResult = $oClient->getMetadata($sPath.'/'.$sName);
474: }
475:
476: return $mResult;
477: }
478:
479:
480: /**
481: * @ignore
482: * @todo not used
483: * @param object $oAccount
484: * @param string $sType
485: * @param string $sPath
486: * @param string $sName
487: * @param boolean $mResult
488: * @param boolean $bBreak
489: */
490: public function onAfterGetFileInfo($aArgs, &$mResult)
491: {
492: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
493:
494: if (self::$sStorageType === $aArgs['Type']) {
495: $mFileInfo = $this->_getFileInfo($aArgs['Path'], $aArgs['Id']);
496: if ($mFileInfo) {
497: $mResult = $this->PopulateFileInfo($mFileInfo);
498: }
499: return true;
500: }
501: }
502:
503: /**
504: * @ignore
505: * @todo not used
506: * @param object $oItem
507: * @return boolean
508: */
509: public function onAfterPopulateFileItem($aArgs, &$oItem)
510: {
511: if ($oItem->IsLink) {
512: if (false !== strpos($oItem->LinkUrl, 'dl.dropboxusercontent.com') ||
513: false !== strpos($oItem->LinkUrl, 'dropbox.com')) {
514: $aMetadata = $this->getMetadataLink($oItem->LinkUrl);
515: if (isset($aMetadata['path']) && $aMetadata['is_dir']) {
516: $oItem->UnshiftAction(array(
517: 'list' => array()
518: ));
519:
520: $oItem->Thumb = true;
521: $oItem->ThumbnailUrl = \MailSo\Base\Http::SingletonInstance()->GetFullUrl() . 'modules/' . self::GetName() . '/images/dropbox.png';
522: }
523: $oItem->LinkType = 'dropbox';
524: return true;
525: }
526: }
527:
528: return false;
529: }
530:
531: protected function getMetadataLink($sLink)
532: {
533: $oClient = $this->getClient();
534: $response = $oClient->postToAPI(
535: '/2/sharing/get_shared_link_metadata',
536: array(
537: 'url' => $sLink
538: )
539: );
540:
541: if ($response->getHttpStatusCode() === 404) {
542: return null;
543: }
544: if ($response->getHttpStatusCode() !== 200) {
545: return null;
546: }
547:
548: $metadata = $response->getDecodedBody();
549: if (array_key_exists("is_deleted", $metadata) && $metadata["is_deleted"]) {
550: return null;
551: }
552: return $metadata;
553: }
554:
555: public function CheckUrlFile(&$aArgs, &$mResult)
556: {
557: if (strpos($aArgs['Path'], '.url') !== false) {
558: list($sUrl, $sPath) = explode('.url', $aArgs['Path']);
559: $sUrl .= '.url';
560: $aArgs['Path'] = $sUrl;
561: $this->prepareArgs($aArgs);
562: if ($sPath) {
563: $aArgs['Path'] .= $sPath;
564: }
565: }
566: }
567:
568: protected function prepareArgs(&$aData)
569: {
570: $aPathInfo = pathinfo($aData['Path']);
571: $sExtension = isset($aPathInfo['extension']) ? $aPathInfo['extension'] : '';
572: if ($sExtension === 'url') {
573: $aArgs = array(
574: 'UserId' => $aData['UserId'],
575: 'Type' => $aData['Type'],
576: 'Path' => $aPathInfo['dirname'],
577: 'Name' => $aPathInfo['basename'],
578: 'IsThumb' => false
579: );
580: $mResult = false;
581: \Aurora\System\Api::GetModuleManager()->broadcastEvent(
582: 'Files',
583: 'GetFile',
584: $aArgs,
585: $mResult
586: );
587: if (is_resource($mResult)) {
588: $aUrlFileInfo = \Aurora\System\Utils::parseIniString(stream_get_contents($mResult));
589: if ($aUrlFileInfo && isset($aUrlFileInfo['URL'])) {
590: if (false !== strpos($aUrlFileInfo['URL'], 'dl.dropboxusercontent.com') ||
591: false !== strpos($aUrlFileInfo['URL'], 'dropbox.com')) {
592: $aData['Type'] = 'dropbox';
593: $aMetadata = $this->getMetadataLink($aUrlFileInfo['URL']);
594: if (isset($aMetadata['path'])) {
595: $aData['Path'] = $aMetadata['path'];
596: }
597: }
598: }
599: }
600: }
601: }
602: /***** private functions *****/
603:
604: /**
605: * Passes data to connect to service.
606: *
607: * @ignore
608: * @param string $aArgs Service type to verify if data should be passed.
609: * @param boolean|array $mResult variable passed by reference to take the result.
610: */
611: public function onGetSettings($aArgs, &$mResult)
612: {
613: $oUser = \Aurora\System\Api::getAuthenticatedUser();
614:
615: if (!empty($oUser)) {
616: $aScope = array(
617: 'Name' => 'storage',
618: 'Description' => $this->i18N('SCOPE_FILESTORAGE'),
619: 'Value' => false
620: );
621: if ($oUser->Role === \Aurora\System\Enums\UserRole::SuperAdmin) {
622: $aScope['Value'] = $this->issetScope('storage');
623: $mResult['Scopes'][] = $aScope;
624: }
625: if ($oUser->isNormalOrTenant()) {
626: if ($aArgs['OAuthAccount'] instanceof \Aurora\Modules\OAuthIntegratorWebclient\Models\OauthAccount) {
627: $aScope['Value'] = $aArgs['OAuthAccount']->issetScope('storage');
628: }
629: if ($this->issetScope('storage')) {
630: $mResult['Scopes'][] = $aScope;
631: }
632: }
633: }
634: }
635:
636: public function onAfterUpdateSettings($aArgs, &$mResult)
637: {
638: $sScope = '';
639: if (isset($aArgs['Scopes']) && is_array($aArgs['Scopes'])) {
640: foreach ($aArgs['Scopes'] as $aScope) {
641: if ($aScope['Name'] === 'storage') {
642: if ($aScope['Value']) {
643: $sScope = 'storage';
644: break;
645: }
646: }
647: }
648: }
649: $this->setConfig('Scopes', $sScope);
650: $this->saveModuleConfig();
651: }
652:
653: /**
654: * Checks if storage type is personal.
655: *
656: * @param string $Type Storage type.
657: * @return bool
658: */
659: protected function checkStorageType($Type)
660: {
661: return $Type === static::$sStorageType;
662: }
663:
664: /**
665: * @ignore
666: * @param array $aArgs Arguments of event.
667: * @param mixed $mResult Is passed by reference.
668: */
669: public function onAfterCheckQuota($aArgs, &$mResult)
670: {
671: $Type = $aArgs['Type'];
672: if ($this->checkStorageType($Type)) {
673: $mResult = true;
674: return true;
675: }
676: }
677: }
678: