/home/ivoiecob/email.hirewise-va.com/modules/S3Filestorage/Module.php
<?php
/**
 * This code is licensed under AGPLv3 license or Afterlogic Software License
 * if commercial version of the product was purchased.
 * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
 */

namespace Aurora\Modules\S3Filestorage;

use Afterlogic\DAV\Constants;
use Afterlogic\DAV\FS\Shared\File as SharedFile;
use Afterlogic\DAV\FS\Shared\Directory as SharedDirectory;
use Aurora\Api;
use Aws\S3\S3Client;
use Aurora\System\Exceptions\ApiException;
use Aurora\Modules\PersonalFiles\Module as PersonalFiles;

/**
 * Adds ability to work with S3 file storage inside Aurora Files module.
 *
 * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
 * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
 * @copyright Copyright (c) 2023, Afterlogic Corp.
 *
 * @property Settings $oModuleSettings
 *
 * @package Modules
 */
class Module extends PersonalFiles
{
    protected $oClient = null;
    protected $sUserPublicId = null;

    protected $sBucketPrefix;
    protected $sBucket;
    protected $sRegion;
    protected $sHost;
    protected $sAccessKey;
    protected $sSecretKey;

    protected $oTenantForDelete = null;
    protected $oUserForDelete = null;

    protected $sTenantName;

    /***** private functions *****/
    /**
     * Initializes Module.
     *
     * @ignore
     */
    public function init()
    {
        $personalFiles = PersonalFiles::getInstance();
        if ($personalFiles && !$this->oModuleSettings->Disabled) {
            $personalFiles->setConfig('Disabled', true);
        }

        parent::init();

        $this->subscribeEvent('Core::DeleteTenant::before', array($this, 'onBeforeDeleteTenant'));
        $this->subscribeEvent('Core::DeleteTenant::after', array($this, 'onAfterDeleteTenant'));

        $this->subscribeEvent('Core::DeleteUser::before', array($this, 'onBeforeDeleteUser'));
        $this->subscribeEvent('Core::DeleteUser::after', array($this, 'onAfterDeleteUser'));

        $this->subscribeEvent('AddToContentSecurityPolicyDefault', array($this, 'onAddToContentSecurityPolicyDefault'));

        $sTenantName = $this->getTenantName();
        $sTenantName = $sTenantName ? $sTenantName : '';
        $this->sBucketPrefix = $this->oModuleSettings->BucketPrefix;
        $this->sBucket = \strtolower($this->sBucketPrefix . \str_replace([' ', '.'], '-', $sTenantName));
        $this->sHost = $this->oModuleSettings->Host;
        $this->sRegion = $this->oModuleSettings->Region;
        $this->sAccessKey = $this->oModuleSettings->AccessKey;
        $this->sSecretKey = $this->oModuleSettings->SecretKey;
    }

    /**
     * @return Module
     */
    public static function getInstance()
    {
        return parent::getInstance();
    }

    /**
     * @return Module
     */
    public static function Decorator()
    {
        return parent::Decorator();
    }

    /**
     * @return Settings
     */
    public function getModuleSettings()
    {
        return $this->oModuleSettings;
    }

    public function onAddToContentSecurityPolicyDefault($aArgs, &$aAddDefault)
    {
        $aAddDefault[] = "https://" . $this->sHost;
        $aAddDefault[] = "https://" . $this->sBucket . "." . $this->sHost;
    }

    /**
     * Obtains list of module settings for authenticated user.
     *
     * @param int|null $TenantId Tenant ID
     *
     * @return array
     */
    public function GetSettings($TenantId = null)
    {
        \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::TenantAdmin);

        $oSettings = $this->oModuleSettings;

        // TODO: temporary desabled getting tenant setting. It must return all settings, not only region and host
        // $aSettings = [];
        // if (!empty($TenantId)) {
        //     \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::TenantAdmin);
        //     $oTenant = \Aurora\System\Api::getTenantById($TenantId);
        //     $oAuthenticatedUser = \Aurora\System\Api::getAuthenticatedUser();

        //     if ($oTenant && ($oAuthenticatedUser->isAdmin() || $oAuthenticatedUser->IdTenant === $oTenant->Id)) {
        //         $aSettings = [
        //             'Region' => $oSettings->GetTenantValue($oTenant->Name, 'Region', ''),
        //             'Host' => $oSettings->GetTenantValue($oTenant->Name, 'Host', ''),
        //         ];
        //     }
        // } else {
        \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::SuperAdmin);
        $aSettings = [
            'AccessKey' => $oSettings->AccessKey,
            'SecretKey' => $oSettings->SecretKey,
            'Region' => $oSettings->Region,
            'Host' => $oSettings->Host,
            'BucketPrefix' => $oSettings->BucketPrefix,
        ];
        // }


        return $aSettings;
    }

    /**
     * Updates module's settings - saves them to config.json file.
     *
     * @param string $AccessKey
     * @param string $SecretKey
     * @param string $Region
     * @param string $Host
     * @param string $BucketPrefix
     * @param int|null $TenantId
     *
     * @return boolean
     */
    public function UpdateS3Settings($AccessKey, $SecretKey, $Region, $Host, $BucketPrefix, $TenantId = null)
    {
        $oSettings = $this->oModuleSettings;

        // TODO: temporary desabled saving tenant setting. It must save all settings, not only region and host
        // if (!empty($TenantId)) {
        // \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::TenantAdmin);
        // $oTenant = \Aurora\System\Api::getTenantById($TenantId);
        // $oAuthenticatedUser = \Aurora\System\Api::getAuthenticatedUser();

        // if ($oTenant && ($oAuthenticatedUser->isAdmin() || $oAuthenticatedUser->IdTenant === $oTenant->Id)) {
        //     return $oSettings->SaveTenantSettings($oTenant->Name, [
        //         'Region' => $Region,
        //         'Host' => $Host
        //     ]);
        // }
        // } else {
        \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::SuperAdmin);

        $oSettings->AccessKey = $AccessKey;
        $oSettings->SecretKey = $SecretKey;
        $oSettings->Region = $Region;
        $oSettings->Host = $Host;
        $oSettings->BucketPrefix = $BucketPrefix;
        return $oSettings->Save();
        // }

        // return false;
    }

    public function GetUsersFolders($iTenantId)
    {
        \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::TenantAdmin);

        $oUser = \Aurora\System\Api::getAuthenticatedUser();
        if ($oUser->Role === \Aurora\System\Enums\UserRole::TenantAdmin && $oUser->IdTenant !== $iTenantId) {
            throw new ApiException(\Aurora\System\Notifications::AccessDenied, null, 'AccessDenied');
        } else {
            Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::SuperAdmin);
        }

        if (!empty($iTenantId)) {
            $this->sBucket = $this->getBucketForTenant($iTenantId);

            $results = $this->getClient(true)->listObjectsV2([
                'Bucket' => $this->getBucketForTenant($iTenantId),
                'Prefix' => '',
                'Delimiter' => '/'
            ]);

            $aUsersFolders = [];
            if (is_array($results['CommonPrefixes']) && count($results['CommonPrefixes']) > 0) {
                foreach ($results['CommonPrefixes'] as $aPrefix) {
                    if (substr($aPrefix['Prefix'], -1) === '/') {
                        $aUsersFolders[] = \rtrim($aPrefix['Prefix'], '/');
                    }
                }
            }

            return $aUsersFolders;
        } else {
            throw new ApiException(\Aurora\System\Notifications::InvalidInputParameter);
        }
    }


    protected function getS3Client()
    {
        $options = [
            'region' => $this->sRegion,
            'version' => 'latest',
            'credentials' => [
                'key'    => $this->sAccessKey,
                'secret' => $this->sSecretKey,
            ]
        ];
        if (!empty($this->sHost)) {
            $options['endpoint'] = 'https://' . $this->sHost;
        }
        return new S3Client($options);
    }

    /**
     * Obtains DropBox client if passed $sType is DropBox account type.
     *
     * @param boolean $bRenew
     * @return S3Client
     */
    protected function getClient($bRenew = false)
    {
        if ($this->oClient === null || $bRenew) {
            \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);

            $this->oClient = $this->getS3Client();

            if (!$this->oClient->doesBucketExist($this->sBucket)) {
                $sBucketLocation = $this->oModuleSettings->BucketLocation;

                $aOptions = [
                    'Bucket' => $this->sBucket,
                ];

                if (!empty($sBucketLocation)) {
                    $aOptions['CreateBucketConfiguration'] = [
                        'LocationConstraint' => $sBucketLocation,
                    ];
                }

                $this->oClient->createBucket($aOptions);

                $res = $this->oClient->putBucketCors([
                    'Bucket' => $this->sBucket,
                    'CORSConfiguration' => [
                        'CORSRules' => [
                            [
                                'AllowedHeaders' => [
                                    '*',
                                ],
                                'AllowedMethods' => [
                                    'GET',
                                    'PUT',
                                    'POST',
                                    'DELETE',
                                    'HEAD'
                                ],
                                'AllowedOrigins' => [
                                    (\Aurora\System\Api::isHttps() ? "https" : "http") . "://" . $_SERVER['HTTP_HOST']
                                ],
                                'MaxAgeSeconds' => 0,
                            ],
                        ],
                    ],
                    'ContentMD5' => '',
                ]);
            }
        }

        return $this->oClient;
    }

    protected function getUserPublicId()
    {
        if (!isset($this->sUserPublicId)) {
            $oUser = \Aurora\System\Api::getAuthenticatedUser();
            $this->sUserPublicId = $oUser->PublicId;
        }

        return $this->sUserPublicId;
    }

    /**
     * getTenantName
     *
     * @return string
     */
    protected function getTenantName()
    {
        if (!isset($this->sTenantName)) {
            $this->sTenantName = \Aurora\System\Api::getTenantName();
        }

        return $this->sTenantName;
    }

    protected function getBucketForTenant($iIdTenant)
    {
        $mResult = false;
        $oTenant = \Aurora\Api::getTenantById($iIdTenant);
        if ($oTenant instanceof \Aurora\Modules\Core\Models\Tenant) {
            $mResult = \strtolower($this->sBucketPrefix . \str_replace([' ', '.'], '-', $oTenant->Name));
        }

        return $mResult;
    }

    protected function copyObject($sFromPath, $sToPath, $sOldName, $sNewName, $bIsFolder = false, $bMove = false)
    {
        $mResult = false;

        $sUserPublicId = $this->getUserPublicId();

        $sSuffix = $bIsFolder ? '/' : '';

        $sFullFromPath = $this->sBucket . '/' . $sUserPublicId . $sFromPath . '/' . $sOldName . $sSuffix;
        $sFullToPath = $sUserPublicId . $sToPath . '/' . $sNewName . $sSuffix;

        $oClient = $this->getClient();

        $oObject = $oClient->headObject([
            'Bucket' => $this->sBucket,
            'Key' => urldecode($sUserPublicId . $sFromPath . '/' . $sOldName . $sSuffix)
        ]);

        $aMetadata = [];
        $sMetadataDirective = 'COPY';
        if ($oObject) {
            $aMetadata = $oObject->get('Metadata');
            $aMetadata['filename'] = $sNewName;
            $sMetadataDirective = 'REPLACE';
        }

        $res = $oClient->copyObject([
            'Bucket' => $this->sBucket,
            'Key' => $sFullToPath,
            'CopySource' => $sFullFromPath,
            'Metadata' => $aMetadata,
            'MetadataDirective' => $sMetadataDirective
        ]);

        if ($res) {
            if ($bMove) {
                $res = $oClient->deleteObject([
                    'Bucket' => $this->sBucket,
                    'Key' => $sUserPublicId . $sFromPath . '/' . $sOldName . $sSuffix
                ]);
            }
            $mResult = true;
        }

        return $mResult;
    }

    protected function getDirectory($sUserPublicId, $sType, $sPath = '')
    {
        $oDirectory = null;

        if ($sUserPublicId) {
            $oDirectory = \Afterlogic\DAV\Server::getNodeForPath('files/' . $sType . '/' . \trim($sPath, '/'), $sUserPublicId);
        }

        return $oDirectory;
    }

    protected function copy($UserId, $FromType, $FromPath, $FromName, $ToType, $ToPath, $ToName, $IsMove = false)
    {
        $sUserPublicId = \Aurora\Api::getUserPublicIdById($UserId);

        $sPath = 'files/' . $FromType . $FromPath . '/' . $FromName;
        $oItem = \Afterlogic\DAV\Server::getNodeForPath($sPath, $sUserPublicId);

        $oToDirectory = $this->getDirectory($sUserPublicId, $ToType, $ToPath);
        $bIsSharedFile = ($oItem instanceof SharedFile || $oItem instanceof SharedDirectory);
        $bIsSharedToDirectory = ($oToDirectory instanceof SharedDirectory);
        $iNotPossibleToMoveSharedFileToSharedFolder = 0;
        if (class_exists('\Aurora\Modules\SharedFiles\Enums\ErrorCodes')) {
            $iNotPossibleToMoveSharedFileToSharedFolder = \Aurora\Modules\SharedFiles\Enums\ErrorCodes::NotPossibleToMoveSharedFileToSharedFolder;
        }
        if ($IsMove && $bIsSharedFile && $bIsSharedToDirectory) {
            throw new ApiException($iNotPossibleToMoveSharedFileToSharedFolder);
        }

        if (($oItem instanceof SharedFile || $oItem instanceof SharedDirectory) && !$oItem->isInherited()) {
            $oPdo = new \Afterlogic\DAV\FS\Backend\PDO();
            $oPdo->updateSharedFileSharePath(Constants::PRINCIPALS_PREFIX . $sUserPublicId, $oItem->getName(), $ToName, $FromPath, $ToPath, $oItem->getGroupId());

            $oItem = $oItem->getNode();
        } else {
            $ToName = $this->getManager()->getNonExistentFileName(
                $sUserPublicId,
                $ToType,
                $ToPath,
                $ToName
            );
            if (!$bIsSharedToDirectory) {
                $oItem->copyObjectTo($ToType, $ToPath, $ToName);
            } else {
                $oToDirectory->createFile($ToName, $oItem->get());
            }
            $oPdo = new \Afterlogic\DAV\FS\Backend\PDO();
            $oPdo->updateShare(Constants::PRINCIPALS_PREFIX . $sUserPublicId, $FromType, $FromPath . '/' . $FromName, $ToType, $ToPath . '/' . $ToName);
            if ($IsMove) {
                \Afterlogic\DAV\Server::deleteNode($sPath, $sUserPublicId);
            }
        }
    }

    // /**
    //  * Moves file if $aData['Type'] is DropBox account type.
    //  *
    //  * @ignore
    //  * @param array $aData
    //  */
    // public function onAfterMove(&$aArgs, &$mResult)
    // {
    // 	\Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);

    // 	if ($this->checkStorageType($aArgs['FromType']))
    // 	{
    // 		$mResult = false;

    // 		$UserId = $aArgs['UserId'];
    // 		Api::CheckAccess($UserId);

    // 		// if ($aArgs['ToType'] === $aArgs['FromType'])
    // 		// {
    // 			foreach ($aArgs['Files'] as $aFile)
    // 			{
    // 				$ToName = isset($aFile['NewName']) ? $aFile['NewName'] : $aFile['Name'];
    // 				$this->copy($UserId, $aArgs['FromType'], $aFile['FromPath'], $aFile['Name'], $aArgs['ToType'], $aArgs['ToPath'], $ToName, true);
    // 			}
    // 			$mResult = true;
    // 		// }
    // 	}

    // }

    // /**
    //  * Copies file if $aData['Type'] is DropBox account type.
    //  *
    //  * @ignore
    //  * @param array $aData
    //  */
    // public function onAfterCopy(&$aArgs, &$mResult)
    // {
    // 	\Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);

    // 	if ($this->checkStorageType($aArgs['FromType']))
    // 	{
    // 		$mResult = false;

    // 		$UserId = $aArgs['UserId'];
    // 		Api::CheckAccess($UserId);

    // 		// if ($aArgs['ToType'] === $aArgs['FromType'])
    // 		// {
    // 			foreach ($aArgs['Files'] as $aFile)
    // 			{
    // 				$ToName = isset($aFile['NewName']) ? $aFile['NewName'] : $aFile['Name'];
    // 				$this->copy($UserId, $aArgs['FromType'], $aFile['FromPath'], $aFile['Name'], $aArgs['ToType'], $aArgs['ToPath'], $ToName, false);
    // 			}
    // 			$mResult = true;
    // 		// }
    // 	}
    // }

    /**
     * @ignore
     * @param array $aArgs Arguments of event.
     * @param mixed $mResult Is passed by reference.
     */
    public function onAfterGetQuota($aArgs, &$mResult)
    {
        if ($this->checkStorageType($aArgs['Type'])) {
            $aQuota = [0, 0];
            $oNode = \Afterlogic\DAV\Server::getNodeForPath('files/' . static::$sStorageType);

            if (is_a($oNode, 'Afterlogic\\DAV\\FS\\S3\\' . ucfirst(static::$sStorageType) . '\\Root')) {
                $aQuota = $oNode->getQuotaInfo();
            }
            $iSpaceLimitMb = $aQuota[1];

            $aArgs = [
                'UserId' => \Aurora\System\Api::getAuthenticatedUserId()
            ];
            $this->broadcastEvent(
                'GetUserSpaceLimitMb',
                $aArgs,
                $iSpaceLimitMb
            );

            $mResult = [
                'Used' => $aQuota[0],
                'Limit' => $iSpaceLimitMb
            ];
        }
    }

    /**
     * @ignore
     * @param array $aArgs Arguments of event.
     * @param mixed $mResult Is passed by reference.
     */
    public function onAfterGetSubModules($aArgs, &$mResult)
    {
        array_unshift($mResult, 's3.' . static::$sStorageType);
    }


    protected function isNeedToReturnBody()
    {
        $sMethod = $this->oHttp->GetPost('Method', null);

        return ((string) \Aurora\System\Router::getItemByIndex(2, '') === 'thumb' ||
            $sMethod === 'SaveFilesAsTempFiles' ||
            $sMethod === 'GetFilesForUpload'
        );
    }

    protected function isNeedToReturnWithContectDisposition()
    {
        $sAction = (string) \Aurora\System\Router::getItemByIndex(2, 'download');
        return $sAction ===  'download';
    }

    protected function deleteUserFolder($IdTenant, $PublicId)
    {
        $bResult = false;
        try {
            $oS3Client = $this->getS3Client();
            $res = $oS3Client->deleteMatchingObjects(
                $this->getBucketForTenant($IdTenant),
                $PublicId . '/'
            );
            $bResult = true;
        } catch(\Exception $oEx) {
            $bResult = false;
        }

        return $bResult;
    }

    /**
     * Puts file content to $mResult.
     * @ignore
     * @param array $aArgs Arguments of event.
     * @param mixed $mResult Is passed by reference.
     */
    public function onGetFile($aArgs, &$mResult)
    {
        if ($this->checkStorageType($aArgs['Type'])) {
            $UserId = $aArgs['UserId'];
            Api::CheckAccess($UserId);

            $sUserPiblicId = \Aurora\Api::getUserPublicIdById($UserId);

            try {
                $sPath = 'files/' . $aArgs['Type'] . $aArgs['Path'] . '/' . $aArgs['Name'];
                /** @var \Afterlogic\DAV\FS\S3\File $oNode */
                $oNode = \Afterlogic\DAV\Server::getNodeForPath($sPath, $sUserPiblicId);

                if ($oNode instanceof \Afterlogic\DAV\FS\File) {
                    $sExt = \pathinfo($aArgs['Name'], PATHINFO_EXTENSION);

                    $oS3FilestorageModule = \Aurora\System\Api::GetModule('S3Filestorage');
                    $bRedirectToUrl = $oS3FilestorageModule ? $oS3FilestorageModule->getConfig('RedirectToOriginalFileURLs', true) : true;

                    $bNoRedirect = isset($aArgs['NoRedirect']) ? $aArgs['NoRedirect'] : !$bRedirectToUrl;

                    if ($this->isNeedToReturnBody() || \strtolower($sExt) === 'url' || $bNoRedirect) {
                        $mResult = $oNode->get();
                    } else {
                        $sUrl = $oNode->getUrl($this->isNeedToReturnWithContectDisposition());
                        if (!empty($sUrl)) {
                            \Aurora\System\Api::Location($sUrl);
                            exit;
                        }
                    }
                }
            } catch (\Sabre\DAV\Exception\NotFound $oEx) {
                $mResult = false;
                //				echo(\Aurora\System\Managers\Response::GetJsonFromObject('Json', \Aurora\System\Managers\Response::FalseResponse(__METHOD__, 404, 'Not Found')));
                $this->oHttp->StatusHeader(404);
                exit;
            }

            return true;
        }
    }

    public function onBeforeDeleteTenant($aArgs, &$mResult)
    {
        $this->oTenantForDelete = \Aurora\Api::getTenantById($aArgs['TenantId']);
    }

    public function onAfterDeleteTenant($aArgs, &$mResult)
    {
        if ($this->oTenantForDelete instanceof \Aurora\Modules\Core\Models\Tenant) {
            try {
                $oS3Client = $this->getS3Client();
                $oS3Client->deleteBucket([
                    'Bucket' => \strtolower($this->sBucketPrefix . \str_replace([' ', '.'], '-', $this->oTenantForDelete->Name))
                ]);
                $this->oTenantForDelete = null;
            } catch(\Exception $oEx) {
            }
        }
    }

    public function onBeforeDeleteUser($aArgs, &$mResult)
    {
        if (isset($aArgs['UserId'])) {
            $this->oUserForDelete = \Aurora\System\Api::getUserById($aArgs['UserId']);
        }
    }

    public function onAfterDeleteUser($aArgs, $mResult)
    {
        if ($this->oUserForDelete instanceof \Aurora\Modules\Core\Models\User) {
            if ($this->deleteUserFolder($this->oUserForDelete->IdTenant, $this->oUserForDelete->PublicId)) {
                $this->oUserForDelete = null;
            }
        }
    }

    public function TestConnection($Region, $Host, $AccessKey = null, $SecretKey = null, $TenantId = null)
    {
        $mResult = true;

        if (isset($TenantId)) {
            \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::TenantAdmin);

            $oTenant = \Aurora\System\Api::getTenantById($TenantId);

            if ($oTenant) {
                $AccessKey = $this->oModuleSettings->AccessKey;
                $SecretKey = $this->oModuleSettings->SecretKey;
            }
        } else {
            \Aurora\System\Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::SuperAdmin);
        }
        try {
            $options = [
                'region' => $Region,
                'version' => 'latest',
                'credentials' => [
                    'key'    => $AccessKey,
                    'secret' => $SecretKey,
                ]
            ];
            if (!empty($Host)) {
                $options['endpoint'] = 'https://' . $Host;
            }
            $s3Client = new S3Client($options);

            $buckets = $s3Client->listBuckets();
        } catch(\Exception $e) {
            $mResult = false;
            Api::LogException($e);
        }
        return $mResult;
    }
}