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\CoreParanoidEncryptionWebclientPlugin;
9:
10: use Aurora\Api;
11: use Aurora\Modules\Files\Classes\FileItem;
12: use Aurora\System\Exceptions\ApiException;
13:
14: /**
15: * Paranoid Encryption module allows you to encrypt files in File module using client-based functionality only.
16: *
17: * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
18: * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
19: * @copyright Copyright (c) 2023, Afterlogic Corp.
20: *
21: * @property Settings $oModuleSettings
22: *
23: * @package Modules
24: */
25: class Module extends \Aurora\System\Module\AbstractWebclientModule
26: {
27: public static $sStorageType = 'encrypted';
28: public static $iStorageOrder = 10;
29: public static $sPersonalStorageType = 'personal';
30: public static $sSharedStorageType = 'shared';
31: public static $sEncryptedFolder = '.encrypted';
32: protected $aRequireModules = ['PersonalFiles','S3Filestorage'];
33:
34: public function init()
35: {
36: $this->subscribeEvent('Files::GetStorages::after', [$this, 'onAfterGetStorages'], 1);
37: $this->subscribeEvent('Files::FileItemtoResponseArray', [$this, 'onFileItemToResponseArray']);
38:
39: $this->subscribeEvent('Files::GetFile', [$this, 'onGetFile']);
40: $this->subscribeEvent('Files::CreateFile', [$this, 'onCreateFile']);
41:
42: $this->subscribeEvent('Files::GetItems::before', [$this, 'onBeforeGetItems']);
43: $this->subscribeEvent('Files::GetItems', [$this, 'onGetItems'], 10001);
44: $this->subscribeEvent('Files::Copy::before', [$this, 'onBeforeCopyOrMove']);
45: $this->subscribeEvent('Files::Move::before', [$this, 'onBeforeCopyOrMove']);
46: $this->subscribeEvent('Files::Delete::before', [$this, 'onBeforeDelete']);
47:
48: $this->subscribeEvent('Files::GetFileInfo::before', [$this, 'onBeforeMethod']);
49: $this->subscribeEvent('Files::CreateFolder::before', [$this, 'onBeforeMethod']);
50: $this->subscribeEvent('Files::Rename::before', [$this, 'onBeforeMethod']);
51: $this->subscribeEvent('Files::GetQuota::before', [$this, 'onBeforeMethod']);
52: $this->subscribeEvent('Files::CreateLink::before', [$this, 'onBeforeMethod']);
53: $this->subscribeEvent('Files::GetFileContent::before', [$this, 'onBeforeMethod']);
54: $this->subscribeEvent('Files::IsFileExists::before', [$this, 'onBeforeMethod']);
55: $this->subscribeEvent('Files::CheckQuota::before', [$this, 'onBeforeMethod']);
56: $this->subscribeEvent('Files::CreatePublicLink::before', [$this, 'onBeforeMethod']);
57: $this->subscribeEvent('Files::DeletePublicLink::before', [$this, 'onBeforeMethod']);
58: $this->subscribeEvent('Files::GetPublicFiles::after', [$this, 'onAfterGetPublicFiles']);
59: $this->subscribeEvent('Files::SaveFilesAsTempFiles::after', [$this, 'onAfterSaveFilesAsTempFiles']);
60: $this->subscribeEvent('Files::UpdateExtendedProps::before', [$this, 'onBeforeMethod']);
61: $this->subscribeEvent('OpenPgpFilesWebclient::CreatePublicLink::before', [$this, 'onBeforeMethod']);
62:
63: $this->subscribeEvent('SharedFiles::UpdateShare::before', [$this, 'onBeforeUpdateShare']);
64: $this->subscribeEvent('SharedFiles::CreateSharedFile', [$this, 'onCreateOrUpdateSharedFile']);
65: $this->subscribeEvent('SharedFiles::UpdateSharedFile', [$this, 'onCreateOrUpdateSharedFile']);
66:
67: $this->subscribeEvent('Files::GetExtendedProps::before', [$this, 'onBeforeGetExtendedProps']);
68: }
69:
70: /**
71: * @return Module
72: */
73: public static function getInstance()
74: {
75: return parent::getInstance();
76: }
77:
78: /**
79: * @return Module
80: */
81: public static function Decorator()
82: {
83: return parent::Decorator();
84: }
85:
86: /**
87: * @return Settings
88: */
89: public function getModuleSettings()
90: {
91: return $this->oModuleSettings;
92: }
93:
94: protected function getEncryptedPath($sPath)
95: {
96: return '/' . self::$sEncryptedFolder . \ltrim($sPath);
97: }
98:
99: protected function startsWith($haystack, $needle)
100: {
101: return (substr($haystack, 0, strlen($needle)) === $needle);
102: }
103:
104: public function onAfterGetStorages($aArgs, &$mResult)
105: {
106: $oUser = \Aurora\System\Api::getAuthenticatedUser();
107: if ($oUser->getExtendedProp($this->GetName() . '::EnableModule')) {
108: array_unshift($mResult, [
109: 'Type' => static::$sStorageType,
110: 'DisplayName' => $this->i18N('LABEL_STORAGE'),
111: 'IsExternal' => false,
112: 'Order' => static::$iStorageOrder,
113: 'IsDroppable' => false
114: ]);
115: }
116: }
117:
118: public function onGetFile($aArgs, &$mResult)
119: {
120: if ($aArgs['Type'] === self::$sStorageType) {
121: $aArgs['Type'] = self::$sPersonalStorageType;
122: $aArgs['Path'] = $this->getEncryptedPath($aArgs['Path']);
123:
124: $this->GetModuleManager()->broadcastEvent(
125: 'Files',
126: 'GetFile',
127: $aArgs,
128: $mResult
129: );
130: }
131: }
132:
133: public function onCreateFile($aArgs, &$mResult)
134: {
135: if ($aArgs['Type'] === self::$sStorageType) {
136: $aArgs['Type'] = self::$sPersonalStorageType;
137: $aArgs['Path'] = $this->getEncryptedPath($aArgs['Path']);
138:
139: $this->GetModuleManager()->broadcastEvent(
140: 'Files',
141: 'CreateFile',
142: $aArgs,
143: $mResult
144: );
145: }
146: }
147:
148: /**
149: * @ignore
150: * @param array $aArgs Arguments of event.
151: * @param mixed $mResult Is passed by reference.
152: */
153: public function onBeforeGetItems(&$aArgs, &$mResult)
154: {
155: if ($aArgs['Type'] === self::$sStorageType) {
156: $aArgs['Type'] = self::$sPersonalStorageType;
157: $aArgs['Path'] = $this->getEncryptedPath($aArgs['Path']);
158:
159: if (!\Aurora\Modules\Files\Module::Decorator()->IsFileExists($aArgs['UserId'], $aArgs['Type'], '', self::$sEncryptedFolder)) {
160: \Aurora\Modules\Files\Module::Decorator()->CreateFolder($aArgs['UserId'], $aArgs['Type'], '', self::$sEncryptedFolder);
161: }
162: }
163: }
164:
165: /**
166: * @ignore
167: * @param array $aArgs Arguments of event.
168: * @param mixed $mResult Is passed by reference.
169: */
170: public function onGetItems(&$aArgs, &$mResult)
171: {
172: if ($aArgs['Type'] === self::$sPersonalStorageType && $aArgs['Path'] === '' && is_array($mResult)) {
173: foreach ($mResult as $iKey => $oFileItem) {
174: if ($oFileItem instanceof FileItem && $oFileItem->IsFolder && $oFileItem->Name === self::$sEncryptedFolder) {
175: unset($mResult[$iKey]);
176: }
177: if ($oFileItem->Shared) {
178: // \Aurora\Modules\SharedFiles\Models\SharedFile::where();
179: }
180: }
181: }
182: //Encrypted files excluded from shared folders
183: if (
184: $this->oHttp->GetHeader('x-client') !== 'WebClient'
185: && $aArgs['Type'] === self::$sPersonalStorageType
186: && substr($aArgs['Path'], 1, 11) === self::$sEncryptedFolder
187: && is_array($mResult)
188: ) {
189: foreach ($mResult as $iKey => $oFileItem) {
190: if (isset($oFileItem->ExtendedProps) && isset($oFileItem->ExtendedProps['ParanoidKey']) && empty($oFileItem->ExtendedProps['ParanoidKey'])) {
191: unset($mResult[$iKey]);
192: }
193: }
194: }
195: }
196:
197: /**
198: * @ignore
199: * @param array $aArgs Arguments of event.
200: * @param mixed $mResult Is passed by reference.
201: */
202: public function onBeforeCopyOrMove(&$aArgs, &$mResult)
203: {
204: if ($aArgs['FromType'] === self::$sStorageType || $aArgs['ToType'] === self::$sStorageType) {
205: if ($aArgs['FromType'] === self::$sStorageType) {
206: $aArgs['FromType'] = self::$sPersonalStorageType;
207: $aArgs['FromPath'] = $this->getEncryptedPath($aArgs['FromPath']);
208: }
209: if ($aArgs['ToType'] === self::$sStorageType) {
210: $aArgs['ToType'] = self::$sPersonalStorageType;
211: $aArgs['ToPath'] = $this->getEncryptedPath($aArgs['ToPath']);
212: }
213:
214: foreach ($aArgs['Files'] as $iKey => $aItem) {
215: if ($aItem['FromType'] === self::$sStorageType) {
216: $aArgs['Files'][$iKey]['FromType'] = self::$sPersonalStorageType;
217: $aArgs['Files'][$iKey]['FromPath'] = $this->getEncryptedPath($aItem['FromPath']);
218: }
219: }
220: }
221: }
222:
223: /**
224: * @ignore
225: * @param array $aArgs Arguments of event.
226: * @param mixed $mResult Is passed by reference.
227: */
228: public function onBeforeDelete(&$aArgs, &$mResult)
229: {
230: if ($aArgs['Type'] === self::$sStorageType) {
231: $aArgs['Type'] = self::$sPersonalStorageType;
232: $aArgs['Path'] = $this->getEncryptedPath($aArgs['Path']);
233:
234: foreach ($aArgs['Items'] as $iKey => $aItem) {
235: $aArgs['Items'][$iKey]['Path'] = $this->getEncryptedPath($aItem['Path']);
236: }
237: }
238: }
239:
240: /**
241: * @ignore
242: * @param array $aArgs Arguments of event.
243: * @param mixed $mResult Is passed by reference.
244: */
245: public function onBeforeMethod(&$aArgs, &$mResult)
246: {
247: if ($aArgs['Type'] === self::$sStorageType) {
248: $aArgs['Type'] = self::$sPersonalStorageType;
249: if (isset($aArgs['Path'])) {
250: $aArgs['Path'] = $this->getEncryptedPath($aArgs['Path']);
251: }
252: }
253: }
254:
255: public function onBeforeUpdateShare(&$aArgs, &$mResult)
256: {
257: if ($aArgs['Storage'] === self::$sStorageType) {
258: if ($aArgs['IsDir']) {
259: $iErrorCode = 0;
260: if (class_exists('\Aurora\Modules\SharedFiles\Enums\ErrorCodes')) {
261: $iErrorCode = \Aurora\Modules\SharedFiles\Enums\ErrorCodes::NotPossibleToShareDirectoryInEcryptedStorage;
262: }
263: throw new ApiException($iErrorCode);
264: }
265: $aArgs['Storage'] = self::$sPersonalStorageType;
266: $aArgs['Type'] = self::$sPersonalStorageType;
267: $aArgs['Path'] = $this->getEncryptedPath($aArgs['Path']);
268: }
269: }
270:
271: public function onCreateOrUpdateSharedFile(&$aArgs, &$mResult)
272: {
273: if (!empty($aArgs['Share']['ParanoidKeyShared']) && class_exists('\Aurora\Modules\SharedFiles\Models\SharedFile')) {
274: $oSharedFile = \Aurora\Modules\SharedFiles\Models\SharedFile::where('owner', $aArgs['UserPrincipalUri'])
275: ->where('storage', $aArgs['Storage'])
276: ->where('path', $aArgs['FullPath'])
277: ->where('principaluri', 'principals/' . $aArgs['Share']['PublicId'])->first();
278: $oSharedFile->setExtendedProp('ParanoidKeyShared', $aArgs['Share']['ParanoidKeyShared']);
279: $oSharedFile->save();
280: }
281: }
282:
283: /**
284: * @param array $aArgs
285: * @return void
286: */
287: public function onFileItemToResponseArray(&$aArgs)
288: {
289: if (isset($aArgs[0]) && $aArgs[0] instanceof \Aurora\Modules\Files\Classes\FileItem) {
290: if ($this->startsWith($aArgs[0]->Path, '/.encrypted')) {
291: $aArgs[0]->Path = str_replace('/.encrypted', '', $aArgs[0]->Path);
292: $aArgs[0]->FullPath = str_replace('/.encrypted', '', $aArgs[0]->FullPath);
293: $aArgs[0]->TypeStr = self::$sStorageType;
294: }
295: }
296: }
297:
298: public function onAfterSaveFilesAsTempFiles(&$aArgs, &$mResult)
299: {
300: $aResult = [];
301: foreach ($mResult as $oFileData) {
302: foreach ($aArgs['Files'] as $oFileOrigData) {
303: if ($oFileOrigData['Name'] === $oFileData['Name']) {
304: if (isset($oFileOrigData['IsEncrypted']) && $oFileOrigData['IsEncrypted']) {
305: $oFileData['Actions'] = [];
306: $oFileData['ThumbnailUrl'] = '';
307: }
308: }
309: }
310: $aResult[] = $oFileData;
311: }
312: $mResult = $aResult;
313: }
314:
315: /**
316: * @param array $aArgs Arguments of event.
317: * @param mixed $mResult Is passed by reference.
318: */
319: public function onAfterGetPublicFiles(&$aArgs, &$mResult)
320: {
321: if (is_array($mResult) && isset($mResult['Items']) && is_array($mResult['Items'])) { //remove from result all encrypted files
322: $mResult['Items'] = array_filter(
323: $mResult['Items'],
324: function ($FileItem) {
325: return !isset($FileItem->ExtendedProps)
326: || !isset($FileItem->ExtendedProps['InitializationVector']);
327: }
328: );
329: }
330: }
331:
332: public function onBeforeGetExtendedProps(&$aArgs, &$mResult)
333: {
334: if ($aArgs['Type'] === self::$sStorageType) {
335: $aArgs['Type'] = self::$sPersonalStorageType;
336: $aArgs['Path'] = $this->getEncryptedPath($aArgs['Path']);
337: }
338: }
339:
340: /**
341: * Obtains list of module settings for authenticated user.
342: *
343: * @return array
344: */
345: public function GetSettings()
346: {
347: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
348: $aSettings = null;
349: $oUser = \Aurora\System\Api::getAuthenticatedUser();
350: if ($oUser && $oUser->isNormalOrTenant()) {
351: $aSettings = [
352: 'EnableModule' => $oUser->getExtendedProp(self::GetName() . '::EnableModule'),
353: 'DontRemindMe' => $oUser->getExtendedProp(self::GetName() . '::DontRemindMe'),
354: 'EnableInPersonalStorage' => $oUser->getExtendedProp(self::GetName() . '::EnableInPersonalStorage'),
355: 'ChunkSizeMb' => $this->oModuleSettings->ChunkSizeMb,
356: 'AllowMultiChunkUpload' => $this->oModuleSettings->AllowMultiChunkUpload,
357: 'AllowChangeSettings' => $this->oModuleSettings->AllowChangeSettings,
358: 'EncryptionMode' => 3 //temporary brought back this setting for compatibility with current versions of mobile apps
359: ];
360: }
361:
362: return $aSettings;
363: }
364:
365: /**
366: * Updates settings of the Paranoid Encryption Module.
367: *
368: * @param boolean $EnableModule indicates if user turned on Paranoid Encryption Module.
369: * @param boolean $EnableInPersonalStorage
370: * @return boolean
371: */
372: public function UpdateSettings($EnableModule, $EnableInPersonalStorage)
373: {
374: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
375:
376: $iUserId = \Aurora\System\Api::getAuthenticatedUserId();
377: if (0 < $iUserId) {
378: $oUser = \Aurora\Modules\Core\Module::Decorator()->GetUserWithoutRoleCheck($iUserId);
379: $oUser->setExtendedProp(self::GetName() . '::EnableModule', $EnableModule);
380: $oUser->setExtendedProp(self::GetName() . '::EnableInPersonalStorage', $EnableInPersonalStorage);
381: \Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
382: }
383: return true;
384: }
385:
386: /**
387: * Updates DontRemindMe setting of the Paranoid Encryption Module.
388: *
389: * @return boolean
390: */
391: public function DontRemindMe()
392: {
393: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
394:
395: $bResult = false;
396: $iUserId = \Aurora\System\Api::getAuthenticatedUserId();
397: if (0 < $iUserId) {
398: $oUser = \Aurora\Modules\Core\Module::Decorator()->GetUserWithoutRoleCheck($iUserId);
399: $oUser->setExtendedProp(self::GetName() . '::DontRemindMe', true);
400: $bResult = \Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
401: }
402:
403: return $bResult;
404: }
405: }
406: