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