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\OpenPgpFilesWebclient;
9:
10: use Afterlogic\DAV\Server;
11: use Aurora\Modules\Files\Enums\ErrorCodes;
12: use Aurora\Modules\Files\Module as FilesModule;
13: use Aurora\System\Exceptions\ApiException;
14:
15: /**
16: * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
17: * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
18: * @copyright Copyright (c) 2023, Afterlogic Corp.
19: *
20: * @property Settings $oModuleSettings
21: *
22: * @package Modules
23: */
24: class Module extends \Aurora\System\Module\AbstractWebclientModule
25: {
26: protected $aRequireModules = array(
27: 'Files'
28: );
29:
30: private $aPublicFileData = null;
31:
32: private $aHashes = [];
33:
34: /** @var \Aurora\Modules\Files\Module */
35: private $oFilesdecorator = null;
36:
37: public function init()
38: {
39: $this->subscribeEvent('FileEntryPub', array($this, 'onFileEntryPub'));
40: $this->subscribeEvent('Files::PopulateFileItem::after', array($this, 'onAfterPopulateFileItem'));
41: $this->subscribeEvent('Files::CheckUrl', array($this, 'onCheckUrl'), 90);
42: $this->subscribeEvent('Files::DeletePublicLink::after', [$this, 'onAfterDeletePublicLink']);
43: $this->subscribeEvent('Min::DeleteExpiredHashes::before', [$this, 'onBeforeDeleteExpiredHashes']);
44: $this->subscribeEvent('Min::DeleteExpiredHashes::after', [$this, 'onAfterDeleteExpiredHashes']);
45: $this->subscribeEvent('Files::GetPublicFiles::before', [$this, 'onBeforeGetPublicFiles']);
46: $this->subscribeEvent('System::RunEntry::before', array($this, 'onBeforeRunEntry'));
47:
48: $oFilesModule = FilesModule::getInstance();
49: if ($oFilesModule) {
50: $this->aErrors = [
51: ErrorCodes::NotPermitted => $oFilesModule->i18N('INFO_NOTPERMITTED')
52: ];
53: }
54:
55: $this->oFilesdecorator = \Aurora\System\Api::GetModuleDecorator('Files');
56: }
57:
58: /**
59: * @return Module
60: */
61: public static function getInstance()
62: {
63: return parent::getInstance();
64: }
65:
66: /**
67: * @return Module
68: */
69: public static function Decorator()
70: {
71: return parent::Decorator();
72: }
73:
74: /**
75: * @return Settings
76: */
77: public function getModuleSettings()
78: {
79: return $this->oModuleSettings;
80: }
81:
82: private function isUrlFileType($sFileName)
83: {
84: return in_array(pathinfo($sFileName, PATHINFO_EXTENSION), ['url']);
85: }
86:
87: /***** public functions might be called with web API *****/
88: /**
89: * Obtains list of module settings for authenticated user.
90: *
91: * @return array
92: */
93: public function GetSettings()
94: {
95: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
96:
97: $aSettings = array(
98: 'EnableSelfDestructingMessages' => $this->oModuleSettings->EnableSelfDestructingMessages,
99: 'EnablePublicLinkLifetime' => $this->oModuleSettings->EnablePublicLinkLifetime,
100: );
101: $oUser = \Aurora\System\Api::getAuthenticatedUser();
102: if ($oUser && $oUser->isNormalOrTenant()) {
103: if (null !== $oUser->getExtendedProp(self::GetName() . '::EnableModule')) {
104: $aSettings['EnableModule'] = $oUser->getExtendedProp(self::GetName() . '::EnableModule');
105: }
106: }
107: if ($this->aPublicFileData) {
108: $aSettings['PublicFileData'] = $this->aPublicFileData;
109: }
110:
111: return $aSettings;
112: }
113:
114: public function UpdateSettings($EnableModule)
115: {
116: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
117:
118: $oUser = \Aurora\System\Api::getAuthenticatedUser();
119: if ($oUser) {
120: if ($oUser->isNormalOrTenant()) {
121: $oCoreDecorator = \Aurora\Modules\Core\Module::Decorator();
122: $oUser->setExtendedProp(self::GetName() . '::EnableModule', $EnableModule);
123: return $oCoreDecorator->UpdateUserObject($oUser);
124: }
125: if ($oUser->Role === \Aurora\System\Enums\UserRole::SuperAdmin) {
126: return true;
127: }
128: }
129:
130: return false;
131: }
132:
133: public function CreatePublicLink($UserId, $Type, $Path, $Name, $Size, $IsFolder, $Password = '', $RecipientEmail = '', $PgpEncryptionMode = '', $LifetimeHrs = 0)
134: {
135: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
136: $mResult = [];
137: $oUser = \Aurora\System\Api::getAuthenticatedUser();
138: if ($oUser instanceof \Aurora\Modules\Core\Models\User) {
139: if (empty($Type) || empty($Name)) {
140: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
141: }
142: $sID = \Aurora\Modules\Min\Module::generateHashId([$oUser->PublicId, $Type, $Path, $Name]);
143: $oMin = \Aurora\Modules\Min\Module::getInstance();
144: $mMin = $oMin->GetMinByID($sID);
145: if (!empty($mMin['__hash__']) && $mMin['UserId'] && $mMin['UserId'] === $oUser->PublicId) {
146: $mResult['link'] = '?/files-pub/' . $mMin['__hash__'] . '/list';
147: if (!empty($mMin['Password'])) {
148: $mResult['password'] = \Aurora\System\Utils::DecryptValue($mMin['Password']);
149: }
150: } else {
151: $oNode = Server::getNodeForPath('files/' . $Type . '/' . $Path . '/' . $Name);
152: if ($oNode instanceof \Afterlogic\DAV\FS\Shared\Directory) {
153: throw new ApiException(ErrorCodes::NotPermitted);
154: }
155: $aProps = [
156: 'UserId' => $oUser->PublicId,
157: 'Type' => $Type,
158: 'Path' => $Path,
159: 'Name' => $Name,
160: 'Size' => $Size,
161: 'IsFolder' => $IsFolder
162: ];
163: if (!empty($Password)) {
164: $aProps['Password'] = \Aurora\System\Utils::EncryptValue($Password);
165: }
166: if (!empty($RecipientEmail)) {
167: $aProps['RecipientEmail'] = $RecipientEmail;
168: }
169: if (!empty($PgpEncryptionMode)) {
170: $aProps['PgpEncryptionMode'] = $PgpEncryptionMode;
171: }
172: if ($LifetimeHrs === 0) {
173: $sHash = $oMin->createMin(
174: $sID,
175: $aProps,
176: $oUser->Id
177: );
178: } else {
179: $iExpireDate = time() + ((int) $LifetimeHrs * 60 * 60);
180: $sHash = $oMin->createMin(
181: $sID,
182: $aProps,
183: $oUser->Id,
184: $iExpireDate
185: );
186: }
187: $mMin = $oMin->GetMinByHash($sHash);
188: if (!empty($mMin['__hash__'])) {
189: $mResult['link'] = '?/files-pub/' . $mMin['__hash__'] . '/list';
190: if (!empty($mMin['Password'])) {
191: $mResult['password'] = \Aurora\System\Utils::DecryptValue($mMin['Password']);
192: }
193: }
194: }
195: }
196:
197: return $mResult;
198: }
199:
200: public function CreateSelfDestrucPublicLink($UserId, $Subject, $Data, $RecipientEmail, $PgpEncryptionMode, $LifetimeHrs)
201: {
202: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
203: $mResult = [];
204: $oUser = \Aurora\System\Api::getAuthenticatedUser();
205: if ($oUser instanceof \Aurora\Modules\Core\Models\User) {
206: $sID = \Aurora\Modules\Min\Module::generateHashId([$oUser->PublicId, $Subject, $Data]);
207: $oMin = \Aurora\Modules\Min\Module::getInstance();
208: $mMin = $oMin->GetMinByID($sID);
209: if (!empty($mMin['__hash__'])) {
210: $mResult['link'] = '?/files-pub/' . $mMin['__hash__'] . '/list';
211: } else {
212: $aProps = [
213: 'UserId' => $oUser->PublicId,
214: 'Subject' => $Subject,
215: 'Data' => $Data,
216: 'RecipientEmail' => $RecipientEmail,
217: 'PgpEncryptionMode' => $PgpEncryptionMode
218: ];
219: $iExpireDate = time() + ((int) $LifetimeHrs * 60 * 60);
220: $sHash = $oMin->createMin(
221: $sID,
222: $aProps,
223: $oUser->Id,
224: $iExpireDate
225: );
226: $mMin = $oMin->GetMinByHash($sHash);
227: if (!empty($mMin['__hash__'])) {
228: $mResult['link'] = '?/files-pub/' . $mMin['__hash__'] . '/list';
229: }
230: }
231: }
232:
233: return $mResult;
234: }
235:
236: public function ValidatePublicLinkPassword($UserId, $Hash, $Password)
237: {
238: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
239: $bResult = false;
240: $oMin = \Aurora\Modules\Min\Module::getInstance();
241: $mMin = $oMin->GetMinByHash($Hash);
242: if ($mMin && isset($mMin['Password']) && \Aurora\System\Utils::DecryptValue($mMin['Password']) === $Password) {
243: $bResult = true;
244: }
245:
246: return $bResult;
247: }
248:
249: /***** public functions might be called with web API *****/
250:
251: public function onFileEntryPub(&$aData, &$mResult)
252: {
253: if ($aData && isset($aData['UserId'])) {
254: if (isset($aData['ExpireDate'])) {
255: $iExpireDate = (int) $aData['ExpireDate'];
256: if ($iExpireDate > 0 && time() > $iExpireDate) {
257: $oModuleManager = \Aurora\System\Api::GetModuleManager();
258: $sTheme = $oModuleManager->getModuleConfigValue('CoreWebclient', 'Theme');
259: $sResult = \file_get_contents($this->GetPath() . '/templates/Expired.html');
260: $mResult = \strtr($sResult, array(
261: '{{Expired}}' => $this->i18N('HINT_MESSAGE_LINK_EXPIRED'),
262: '{{Theme}}' => $sTheme,
263: ));
264: return;
265: }
266: }
267:
268: $bLinkOrFile = isset($aData['IsFolder']) && !$aData['IsFolder'] && isset($aData['Name']) && isset($aData['Type']) && isset($aData['Path']);
269: $bSelfDestructingEncryptedMessage = isset($aData['Subject']) && isset($aData['Data']) && isset($aData['PgpEncryptionMode']) && isset($aData['RecipientEmail']);
270: if ($bLinkOrFile || $bSelfDestructingEncryptedMessage) {
271: $bIsUrlFile = isset($aData['Name']) ? $this->isUrlFileType($aData['Name']) : false;
272:
273: /** @var \Aurora\Modules\Core\Module $oCoreDecorator */
274: $oCoreDecorator = \Aurora\System\Api::GetModuleDecorator('Core');
275: $oUser = $oCoreDecorator->GetUserByPublicId($aData['UserId']);
276: if ($oUser) {
277: $bPrevState = \Aurora\System\Api::skipCheckUserRole(true);
278:
279: $aCurSession = \Aurora\System\Api::GetUserSession();
280: \Aurora\System\Api::SetUserSession([
281: 'UserId' => $oUser->Id
282: ]);
283:
284: $sType = isset($aData['Type']) ? $aData['Type'] : '';
285: $sPath = isset($aData['Path']) ? $aData['Path'] : '';
286: $sName = isset($aData['Name']) ? $aData['Name'] : '';
287:
288: $aFileInfo = $this->oFilesdecorator->GetFileInfo($aData['UserId'], $sType, $sPath, $sName);
289:
290: \Aurora\System\Api::SetUserSession($aCurSession);
291:
292: \Aurora\System\Api::skipCheckUserRole($bPrevState);
293: $bIsEncyptedFile = $aFileInfo
294: && isset($aFileInfo->ExtendedProps)
295: && isset($aFileInfo->ExtendedProps['ParanoidKeyPublic']);
296: $bIsSetPgpEncryptionMode = isset($aData['PgpEncryptionMode']);
297: $oApiIntegrator = \Aurora\System\Managers\Integrator::getInstance();
298:
299: if ($oApiIntegrator
300: && (($bIsEncyptedFile && $bIsSetPgpEncryptionMode)
301: || !$bIsEncyptedFile)
302: ) {
303: $oCoreClientModule = \Aurora\System\Api::GetModule('CoreWebclient');
304: if ($oCoreClientModule instanceof \Aurora\System\Module\AbstractModule) {
305: $sResult = \file_get_contents($oCoreClientModule->GetPath() . '/templates/Index.html');
306: if (\is_string($sResult)) {
307: $oSettings = &\Aurora\System\Api::GetSettings();
308: $sFrameOptions = $oSettings->XFrameOptions;
309: if (0 < \strlen($sFrameOptions)) {
310: @\header('X-Frame-Options: ' . $sFrameOptions);
311: }
312: $aConfig = [
313: 'public_app' => true,
314: 'modules_list' => array_merge(
315: $oApiIntegrator->GetModulesForEntry('OpenPgpFilesWebclient'),
316: $oApiIntegrator->GetModulesForEntry('OpenPgpWebclient')
317: )
318: ];
319: //passing data to AppData throughGetSettings. GetSettings will be called in $oApiIntegrator->buildBody
320: /** @var \Aurora\Modules\FilesWebclient\Module $oFilesWebclientModule */
321: $oFilesWebclientModule = \Aurora\System\Api::GetModule('FilesWebclient');
322: if ($oFilesWebclientModule) {
323: $sUrl = (bool) $oFilesWebclientModule->oModuleSettings->ServerUseUrlRewrite ? '/download/' : '?/files-pub/';
324: $this->aPublicFileData = [
325: 'Url' => $sUrl . $aData['__hash__'],
326: 'Hash' => $aData['__hash__']
327: ];
328: if ($bSelfDestructingEncryptedMessage) {
329: $this->aPublicFileData['Subject'] = $aData['Subject'];
330: $this->aPublicFileData['Data'] = $aData['Data'];
331: $this->aPublicFileData['PgpEncryptionMode'] = $aData['PgpEncryptionMode'];
332: $this->aPublicFileData['RecipientEmail'] = $aData['RecipientEmail'];
333: $this->aPublicFileData['ExpireDate'] = isset($aData['ExpireDate']) ? $aData['ExpireDate'] : null;
334: } elseif ($bIsEncyptedFile) {
335: $this->aPublicFileData['PgpEncryptionMode'] = $aData['PgpEncryptionMode'];
336: $this->aPublicFileData['PgpEncryptionRecipientEmail'] = isset($aData['RecipientEmail']) ? $aData['RecipientEmail'] : '';
337: $this->aPublicFileData['Size'] = \Aurora\System\Utils::GetFriendlySize($aData['Size']);
338: $this->aPublicFileData['Name'] = $aData['Name'];
339: $this->aPublicFileData['ParanoidKeyPublic'] = $aFileInfo->ExtendedProps['ParanoidKeyPublic'];
340: $this->aPublicFileData['InitializationVector'] = $aFileInfo->ExtendedProps['InitializationVector'];
341: } elseif ($bIsUrlFile) {
342: $mFile = $this->oFilesdecorator->getRawFileData($aData['UserId'], $aData['Type'], $aData['Path'], $aData['Name'], $aData['__hash__'], 'view');
343: if (\is_resource($mFile)) {
344: $mFile = \stream_get_contents($mFile);
345: }
346: $aUrlFileInfo = \Aurora\System\Utils::parseIniString($mFile);
347: if ($aUrlFileInfo && isset($aUrlFileInfo['URL'])) {
348: $sUrl = $aUrlFileInfo['URL'];
349: $sFileName = basename($sUrl);
350: $sFileExtension = \Aurora\System\Utils::GetFileExtension($sFileName);
351: if (\strtolower($sFileExtension) === 'm3u8') {
352: $this->aPublicFileData['Url'] = $sUrl;
353: $this->aPublicFileData['Name'] = $sFileName; #$aData['Name'];
354: $this->aPublicFileData['IsSecuredLink'] = isset($aData['Password']);
355: $this->aPublicFileData['IsUrlFile'] = true;
356: }
357: }
358: } else {//encrypted link
359: $this->aPublicFileData['Size'] = \Aurora\System\Utils::GetFriendlySize($aData['Size']);
360: $this->aPublicFileData['Name'] = $aData['Name'];
361: $this->aPublicFileData['IsSecuredLink'] = isset($aData['Password']);
362: $this->aPublicFileData['ExpireDate'] = isset($aData['ExpireDate']) ? $aData['ExpireDate'] : null;
363: }
364:
365: $mResult = \strtr(
366: $sResult,
367: [
368: '{{AppVersion}}' => \Aurora\System\Application::GetVersion(),
369: '{{IntegratorDir}}' => $oApiIntegrator->isRtl() ? 'rtl' : 'ltr',
370: '{{IntegratorLinks}}' => $oApiIntegrator->buildHeadersLink(),
371: '{{IntegratorBody}}' => $oApiIntegrator->buildBody($aConfig)
372: ]
373: );
374: }
375: }
376: }
377: }
378: }
379: }
380: }
381: }
382:
383: public function onAfterPopulateFileItem($aArgs, &$oItem)
384: {
385: \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
386: if (isset($aArgs['UserId']) &&
387: $oItem instanceof \Aurora\Modules\Files\Classes\FileItem
388: && isset($oItem->TypeStr)
389: ) {
390: $oUser = \Aurora\System\Api::getUserById($aArgs['UserId']);
391: $iAuthenticatedUserId = \Aurora\System\Api::getAuthenticatedUserId();
392: if ($oUser instanceof \Aurora\Modules\Core\Models\User
393: && $iAuthenticatedUserId === $aArgs['UserId']
394: ) {
395: $sID = \Aurora\Modules\Min\Module::generateHashId([$oUser->PublicId, $oItem->TypeStr, $oItem->Path, $oItem->Name]);
396: $mMin = \Aurora\Modules\Min\Module::getInstance()->GetMinByID($sID);
397: if (!empty($mMin['__hash__'])) {
398: $aExtendedProps = array_merge($oItem->ExtendedProps, [
399: 'PasswordForSharing' => !empty($mMin['Password']) ? \Aurora\System\Utils::DecryptValue($mMin['Password']) : '',
400: 'PublicLink' => '?/files-pub/' . $mMin['__hash__'] . '/list',
401: 'PublicLinkPgpEncryptionMode' => isset($mMin['PgpEncryptionMode']) ? $mMin['PgpEncryptionMode'] : '',
402: ]);
403: $oItem->ExtendedProps = $aExtendedProps;
404: }
405: }
406: }
407: }
408:
409: public function onCheckUrl($aArgs, &$mResult)
410: {
411: if (!empty($aArgs['Url'])) {
412: $sUrl = $aArgs['Url'];
413: if ($sUrl) {
414: $sFileName = basename($sUrl);
415: $sFileExtension = \Aurora\System\Utils::GetFileExtension($sFileName);
416: if (\strtolower($sFileExtension) === 'm3u8') {
417: $mResult['Name'] = $sFileName;
418: return true;
419: }
420: }
421: }
422: }
423:
424: public function onAfterDeletePublicLink(&$aArgs, &$mResult)
425: {
426: \Aurora\Modules\Files\Module::Decorator()->UpdateExtendedProps(
427: $aArgs['UserId'],
428: $aArgs['Type'],
429: $aArgs['Path'],
430: $aArgs['Name'],
431: ['ParanoidKeyPublic' => null]
432: );
433: }
434:
435: public function onBeforeDeleteExpiredHashes(&$aArgs, &$mResult)
436: {
437: $this->aHashes = [];
438: if (isset($aArgs['Time']) && $aArgs['Time'] > 0) {
439: $this->aHashes = \Aurora\Modules\Min\Models\MinHash::whereNotNull('ExpireDate')->where('ExpireDate', '<=', $aArgs['Time'])->get()->all();
440: }
441: }
442:
443: public function onAfterDeleteExpiredHashes(&$aArgs, &$mResult)
444: {
445: foreach ($this->aHashes as $hash) {
446: $data = \json_decode($hash['Data'], true);
447: if (isset($data['UserId'], $data['Type'], $data['Path'], $data['Name'])) {
448: \Aurora\Modules\Files\Module::Decorator()->UpdateExtendedProps(
449: $data['UserId'],
450: $data['Type'],
451: $data['Path'],
452: $data['Name'],
453: ['ParanoidKeyPublic' => null]
454: );
455: }
456: }
457: $this->aHashes = [];
458: }
459:
460: public function onBeforeGetPublicFiles(&$aArgs, &$mResult)
461: {
462: /** @var \Aurora\Modules\Min\Module $oMinDecorator */
463: $oMinDecorator = \Aurora\Api::GetModuleDecorator('Min');
464: if ($oMinDecorator) {
465: $mMin = $oMinDecorator->GetMinByHash($aArgs['Hash']);
466: if (!empty($mMin['__hash__'])) {
467: if (isset($mMin['ExpireDate'])) {
468: $iExpireDate = (int) $mMin['ExpireDate'];
469: if ($iExpireDate > 0 && time() > $iExpireDate) {
470: $mResult = false;
471: return true;
472: }
473: }
474: }
475: }
476: }
477:
478: public function onBeforeRunEntry(&$aArgs, &$mResult)
479: {
480: if (isset($aArgs['EntryName']) && strtolower($aArgs['EntryName']) === 'download-file') {
481: $sHash = (string) \Aurora\System\Router::getItemByIndex(1, '');
482: $aValues = \Aurora\System\Api::DecodeKeyValues($sHash);
483: if (isset($aValues['PublicHash'])) {
484: /** @var \Aurora\Modules\Min\Module $oMinDecorator */
485: $oMinDecorator = \Aurora\Api::GetModuleDecorator('Min');
486: if ($oMinDecorator) {
487: $mMin = $oMinDecorator->GetMinByHash($aValues['PublicHash']);
488: if (!empty($mMin['__hash__'])) {
489: if (isset($mMin['ExpireDate'])) {
490: $iExpireDate = (int) $mMin['ExpireDate'];
491: if ($iExpireDate > 0 && time() > $iExpireDate) {
492: $this->oHttp->StatusHeader(403);
493: exit();
494: }
495: }
496: }
497: }
498: }
499: }
500: }
501: }
502: