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