diff --git a/Admin/Install/db.json b/Admin/Install/db.json index ea4acb5..f39d967 100755 --- a/Admin/Install/db.json +++ b/Admin/Install/db.json @@ -122,9 +122,9 @@ "type": "TINYINT", "null": false }, - "media_nonce": { - "name": "media_nonce", - "type": "VARCHAR(255)", + "media_encrypted": { + "name": "media_encrypted", + "type": "TINYINT(1)", "null": true, "default": null }, diff --git a/Admin/Installer.php b/Admin/Installer.php index ef2bf2f..05f8eb8 100755 --- a/Admin/Installer.php +++ b/Admin/Installer.php @@ -57,6 +57,10 @@ final class Installer extends InstallerAbstract \mkdir(__DIR__ . '/../Files'); } + if (!\is_dir(__DIR__ . '/../../../Temp')) { + \mkdir(__DIR__ . '/../../../Temp'); + } + parent::install($app, $info, $cfgHandler); // Create directory for admin account @@ -69,9 +73,9 @@ final class Installer extends InstallerAbstract foreach ($accounts as $account) { $collection = new Collection(); - $collection->name = ((string) $account->getId()) . ' ' . $account->login; + $collection->name = ((string) $account->id) . ' ' . $account->login; $collection->setVirtualPath('/Accounts'); - $collection->setPath('/Modules/Media/Files/Accounts/' . ((string) $account->getId())); + $collection->setPath('/Modules/Media/Files/Accounts/' . ((string) $account->id)); // The installation is always run by the admin account since the module is a "base" module which is always installed during the application setup $collection->createdBy = new NullAccount(1); @@ -261,7 +265,7 @@ final class Installer extends InstallerAbstract } $type = $responseData['response']; - $id = $type->getId(); + $id = $type->id; $isFirst = true; foreach ($data['l11n'] as $l11n) { diff --git a/Admin/Settings/Theme/Backend/settings.tpl.php b/Admin/Settings/Theme/Backend/settings.tpl.php index ab49698..8faa17f 100755 --- a/Admin/Settings/Theme/Backend/settings.tpl.php +++ b/Admin/Settings/Theme/Backend/settings.tpl.php @@ -69,9 +69,9 @@ echo $this->getData('nav')->render(); ?> $type) : ++$count; - $url = UriFactory::build('{/base}/admin/module/settings?id=Media&type=' . $type->getId()); ?> + $url = UriFactory::build('{/base}/admin/module/settings?id=Media&type=' . $type->id); ?> - getId(); ?> + id; ?> printHtml($type->name); ?> printHtml($type->getL11n()); ?> diff --git a/Controller/ApiController.php b/Controller/ApiController.php index e8a2cfd..3af3682 100755 --- a/Controller/ApiController.php +++ b/Controller/ApiController.php @@ -90,7 +90,7 @@ final class ApiController extends Controller basePath: __DIR__ . '/../../../Modules/Media/Files' . \urldecode($request->getDataString('path') ?? ''), virtualPath: \urldecode($request->getDataString('virtualpath') ?? ''), password: $request->getDataString('password') ?? '', - encryptionKey: $request->getDataString('encrypt') ?? '', + encryptionKey: $request->getDataString('encryption') ?? ($request->getDataBool('isencrypted') === true && !empty($_SERVER['OMS_PRIVATE_KEY_I'] ?? '') ? $_SERVER['OMS_PRIVATE_KEY_I'] : ''), pathSettings: $request->getDataInt('pathsettings') ?? PathSettings::RANDOM_PATH, // IMPORTANT!!! hasAccountRelation: $request->getDataBool('link_account') ?? false, readContent: $request->getDataBool('parse_content') ?? false, @@ -99,7 +99,7 @@ final class ApiController extends Controller $ids = []; foreach ($uploads as $file) { - $ids[] = $file->getId(); + $ids[] = $file->id; // add media types if (!empty($types = $request->getDataJson('types'))) { @@ -123,7 +123,7 @@ final class ApiController extends Controller $this->createModelRelation( $request->header->account, - $file->getId(), + $file->id, $tId, MediaMapper::class, 'types', @@ -156,7 +156,7 @@ final class ApiController extends Controller $this->createModelRelation( $request->header->account, - $file->getId(), + $file->id, $tId, MediaMapper::class, 'tags', @@ -321,7 +321,9 @@ final class ApiController extends Controller $virtualPath, app: $hasAccountRelation ? $this->app : null, readContent: $readContent, - unit: $unit + unit: $unit, + password: $password, + isEncrypted: !empty($encryptionKey) ); } @@ -390,7 +392,9 @@ final class ApiController extends Controller string $ip = '127.0.0.1', ApplicationAbstract $app = null, bool $readContent = false, - int $unit = null + int $unit = null, + string $password = '', + bool $isEncrypted = false ) : Media { if (!isset($status['status']) || $status['status'] !== UploadStatus::OK) { @@ -406,6 +410,8 @@ final class ApiController extends Controller $media->extension = $status['extension']; $media->unit = $unit; $media->setVirtualPath($virtualPath); + $media->setPassword($password); + $media->isEncrypted = $isEncrypted; if ($readContent && \is_file($media->getAbsolutePath())) { $content = self::loadFileContent($media->getAbsolutePath(), $media->extension); @@ -430,7 +436,7 @@ final class ApiController extends Controller null, $media, StringUtils::intHash(MediaMapper::class), 'Media-media-create', self::NAME, - (string) $media->getId(), + (string) $media->id, '', $ip ] @@ -444,7 +450,7 @@ final class ApiController extends Controller self::NAME, self::NAME, PermissionCategory::MEDIA, - $media->getId(), + $media->id, null, PermissionType::READ | PermissionType::MODIFY | PermissionType::DELETE | PermissionType::PERMISSION ), @@ -584,7 +590,7 @@ final class ApiController extends Controller $media->setPath($request->getDataString('path') ?? $media->getPath()); $media->setVirtualPath(\urldecode($request->getDataString('virtualpath') ?? $media->getVirtualPath())); - if ($media instanceof NullMedia + if ($media->id === 0 || !$this->app->accountManager->get($request->header->account)->hasPermission( PermissionType::MODIFY, $this->app->unitId, @@ -646,7 +652,7 @@ final class ApiController extends Controller ->where('name', \basename($request->getDataString('virtualpath') ?? '')) ->execute(); - $parentCollectionId = $parentCollection->getId(); + $parentCollectionId = $parentCollection->id; } if (!$request->hasData('source')) { @@ -656,13 +662,13 @@ final class ApiController extends Controller ->where('name', \basename($request->getDataString('child') ?? '')) ->execute(); - $request->setData('source', $child->getId()); + $request->setData('source', $child->id); } $this->createModelRelation( $request->header->account, $parentCollectionId, - $ref->getId(), + $ref->id, CollectionMapper::class, 'sources', '', @@ -912,7 +918,7 @@ final class ApiController extends Controller /** @var Collection $parentCollection */ $parentCollection = CollectionMapper::getParentCollection($temp)->execute(); - if ($parentCollection->getId() > 0) { + if ($parentCollection->id > 0) { break; } @@ -930,8 +936,8 @@ final class ApiController extends Controller $this->createModel($account, $childCollection, CollectionMapper::class, 'collection', '127.0.0.1'); $this->createModelRelation( $account, - $parentCollection->getId(), - $childCollection->getId(), + $parentCollection->id, + $childCollection->id, CollectionMapper::class, 'sources', '', @@ -1011,10 +1017,12 @@ final class ApiController extends Controller $virtualPath, $request->getOrigin(), $this->app, - unit: $request->getDataInt('unit') + unit: $request->getDataInt('unit'), + password: $request->getDataString('password') ?? '', + isEncrypted: $request->getDataBool('isencrypted') ?? $request->hasData('encryption') ); - $ids[] = $created->getId(); + $ids[] = $created->id; } $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Media', 'Media successfully created.', $ids); @@ -1057,15 +1065,16 @@ final class ApiController extends Controller } } - if (!($media instanceof NullMedia)) { - if ($request->header->account !== $media->createdBy->getId() + if ($media->id > 0) { + if (!($data['ignorePermission'] ?? false) + && $request->header->account !== $media->createdBy->id && !$this->app->accountManager->get($request->header->account)->hasPermission( PermissionType::READ, $this->app->unitId, $this->app->appId, self::NAME, PermissionCategory::MEDIA, - $media->getId() + $media->id ) ) { $this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []); @@ -1095,7 +1104,7 @@ final class ApiController extends Controller } if ($media->hasPassword() - && !$media->comparePassword((string) $request->getData('password')) + && !$media->comparePassword($request->getDataString('password')) ) { $view = new View($this->app->l11nManager, $request, $response); $view->setTemplate('/Modules/Media/Theme/Api/invalidPassword'); @@ -1103,6 +1112,17 @@ final class ApiController extends Controller return; } + if ($media->isEncrypted) { + $media = $this->prepareEncryptedMedia($media, $request); + + if ($media->id === 0) { + $this->fillJsonResponse($request, $response, NotificationLevel::ERROR, 'Media', 'Media could not be exported. Please try again.', []); + $response->header->status = RequestStatusCode::R_500; + + return; + } + } + $this->setMediaResponseHeader($media, $request, $response); $view = $this->createView($media, $request, $response); $view->setData('path', __DIR__ . '/../../../'); @@ -1110,6 +1130,38 @@ final class ApiController extends Controller $response->set('export', $view); } + private function prepareEncryptedMedia(Media $media, RequestAbstract $request) : Media + { + $path = ''; + $absolutePath = ''; + + $counter = 0; + do { + $randomName = \sha1(\random_bytes(32)); + + $path = '../../../Temp/' . $randomName . '.' . $media->getExtension(); + $absolutePath = __DIR__ . '/' . $path; + } while(!\is_file($absolutePath) && $counter < 1000); + + if ($counter >= 1000) { + return new NullMedia(); + } + + $encryptionKey = $request->getDataBool('isencrypted') === true && !empty($_SESSION['OMS_PRIVATE_KEY_I'] ?? '') + ? $_SESSION['OMS_PRIVATE_KEY_I'] + : $request->getDataString('encrpkey') ?? ''; + + $decrypted = $media->decrypt($encryptionKey, $absolutePath); + + if (!$decrypted) { + return new NullMedia(); + } + + $media->path = $media->isAbsolute ? $absolutePath : $path; + + return $media; + } + /** * Routing end-point for application behaviour. * diff --git a/Controller/BackendController.php b/Controller/BackendController.php index afb7d80..193679b 100755 --- a/Controller/BackendController.php +++ b/Controller/BackendController.php @@ -124,7 +124,7 @@ final class BackendController extends Controller $collection = $collectionMapper->execute(); - if ((\is_array($collection) || $collection instanceof NullCollection) && \is_dir(__DIR__ . '/../Files' . $path)) { + if ((\is_array($collection) || $collection->id === 0) && \is_dir(__DIR__ . '/../Files' . $path)) { $collection = new Collection(); $collection->name = \basename($path); $collection->setVirtualPath(\dirname($path)); @@ -132,11 +132,11 @@ final class BackendController extends Controller $collection->isAbsolute = false; } - if ($collection instanceof Collection && !($collection instanceof NullCollection)) { + if ($collection instanceof Collection && $collection->id > 0) { $collectionSources = $collection->getSources(); foreach ($collectionSources as $source) { foreach ($media as $obj) { - if ($obj->getId() === $source->getId()) { + if ($obj->id === $source->id) { continue 2; } } @@ -214,6 +214,7 @@ final class BackendController extends Controller if ($id === 0) { $path = \urldecode($request->getDataString('path') ?? ''); $media = new NullMedia(); + if (\is_file(__DIR__ . '/../Files' . $path)) { $name = \explode('.', \basename($path)); @@ -236,14 +237,6 @@ final class BackendController extends Controller ->where('tags/title/language', $request->getLanguage()) ->execute(); - if ($media->hasPassword() - && !$media->comparePassword((string) $request->getData('password')) - ) { - $view->setTemplate('/Modules/Media/Theme/Backend/Components/Media/invalidPassword'); - - return $view; - } - if ($media->class === MediaClass::COLLECTION) { /** @var \Modules\Media\Models\Media[] $files */ $files = MediaMapper::getByVirtualPath( @@ -272,7 +265,7 @@ final class BackendController extends Controller ->with('tags') ->with('tags/title') ->with('content') - ->where('id', $media->source?->getId() ?? 0) + ->where('id', $media->source?->id ?? 0) ->where('tags/title/language', $request->getLanguage()) ->execute(); @@ -311,6 +304,16 @@ final class BackendController extends Controller return $view; } + if ($media->isEncrypted) { + $media = $this->app->moduleManager->get('Media', 'Api')->prepareEncryptedMedia($media, $request); + + if ($media->id === 0) { + $view->setTemplate('/Modules/Media/Theme/Backend/Components/Media/invalidPassword'); + + return $view; + } + } + switch (\strtolower($media->extension)) { case 'pdf': $view->setTemplate('/Modules/Media/Theme/Backend/Components/Media/pdf'); @@ -395,9 +398,7 @@ final class BackendController extends Controller $id = $request->getDataString('id') ?? ''; $settings = SettingMapper::getAll()->where('module', $id)->execute(); - if (!($settings instanceof NullSetting)) { - $view->setData('settings', !\is_array($settings) ? [$settings] : $settings); - } + $view->setData('settings', $settings); $types = MediaTypeMapper::getAll()->with('title')->where('title/language', $response->getLanguage())->execute(); $view->setData('types', $types); @@ -434,7 +435,7 @@ final class BackendController extends Controller $view->addData('nav', $this->app->moduleManager->get('Navigation')->createNavigationMid(1007501001, $request, $response)); $view->addData('type', $type); - $l11n = MediaTypeL11nMapper::getAll()->where('type', $type->getId())->execute(); + $l11n = MediaTypeL11nMapper::getAll()->where('type', $type->id)->execute(); $view->addData('l11n', $l11n); return $view; diff --git a/Models/Media.php b/Models/Media.php index 98c221c..4245607 100755 --- a/Models/Media.php +++ b/Models/Media.php @@ -18,6 +18,7 @@ use Modules\Admin\Models\Account; use Modules\Admin\Models\NullAccount; use Modules\Tag\Models\NullTag; use Modules\Tag\Models\Tag; +use phpOMS\Security\EncryptionHelper; /** * Media class. @@ -35,7 +36,7 @@ class Media implements \JsonSerializable * @var int * @since 1.0.0 */ - protected int $id = 0; + public int $id = 0; /** * Name. @@ -99,7 +100,7 @@ class Media implements \JsonSerializable * @var string * @since 1.0.0 */ - protected string $path = ''; + public string $path = ''; /** * Virtual path. @@ -107,7 +108,7 @@ class Media implements \JsonSerializable * @var string * @since 1.0.0 */ - protected string $virtualPath = '/'; + public string $virtualPath = '/'; /** * Is path absolute? @@ -150,12 +151,12 @@ class Media implements \JsonSerializable public ?Media $source = null; /** - * Media encryption nonce. + * Is encrypted. * - * @var null|string + * @var bool * @since 1.0.0 */ - protected ?string $nonce = null; + public bool $isEncrypted = false; /** * Media password hash. @@ -163,7 +164,7 @@ class Media implements \JsonSerializable * @var null|string * @since 1.0.0 */ - protected ?string $password = null; + public ?string $password = null; /** * Media is hidden. @@ -195,7 +196,7 @@ class Media implements \JsonSerializable * @var Tag[] * @since 1.0.0 */ - protected array $tags = []; + public array $tags = []; /** * Language. @@ -203,7 +204,7 @@ class Media implements \JsonSerializable * @var null|string * @since 1.0.0 */ - protected ?string $language = null; + public ?string $language = null; /** * Country. @@ -211,7 +212,7 @@ class Media implements \JsonSerializable * @var null|string * @since 1.0.0 */ - protected ?string $country = null; + public ?string $country = null; /** * Constructor. @@ -237,57 +238,31 @@ class Media implements \JsonSerializable /** * Encrypt the media file * - * @param string $password Password to encrypt the file with + * @param string $key Password to encrypt the file with * @param null|string $outputPath Output path of the encryption (null = replace file) * - * @return string - * - * @since 1.0.0 - */ - public function encrypt(string $password, string $outputPath = null) : string - { - return ''; - } - - /** - * Decrypt the media file - * - * @param string $password Password to encrypt the file with - * @param null|string $outputPath Output path of the encryption (null = replace file) - * - * @return string - * - * @since 1.0.0 - */ - public function decrypt(string $password, string $outputPath = null) : string - { - return ''; - } - - /** - * Set encryption nonce - * - * @param null|string $nonce Nonce from encryption password - * - * @return void - * - * @since 1.0.0 - */ - public function setNonce(?string $nonce) : void - { - $this->nonce = $nonce; - } - - /** - * Is media file encrypted? - * * @return bool * * @since 1.0.0 */ - public function isEncrypted() : bool + public function encrypt(string $key, string $outputPath = null) : bool { - return $this->nonce !== null; + return EncryptionHelper::encryptFile($this->getAbsolutePath(), $outputPath, $key); + } + + /** + * Decrypt the media file + * + * @param string $key Password to encrypt the file with + * @param null|string $outputPath Output path of the encryption (null = replace file) + * + * @return bool + * + * @since 1.0.0 + */ + public function decrypt(string $key, string $outputPath = null) : bool + { + return EncryptionHelper::decryptFile($this->getAbsolutePath(), $outputPath, $key); } /** @@ -332,20 +307,6 @@ class Media implements \JsonSerializable return \password_verify($password, $this->password ?? ''); } - /** - * Compare nonce with encryption nonce of the media file - * - * @param string $nonce User nonce - * - * @return bool - * - * @since 1.0.0 - */ - public function compareNonce(string $nonce) : bool - { - return $this->nonce === null ? false : \hash_equals($this->nonce, $nonce); - } - /** * Get the media path * @@ -358,6 +319,22 @@ class Media implements \JsonSerializable return $this->isAbsolute ? $this->path : \ltrim($this->path, '\\/'); } + public function getFileName() : string + { + return \basename($this->path); + } + + public function getExtension() : string + { + $pos = \strrpos('.', $this->path); + + if ($pos === false) { + return ''; + } + + return \substr($this->path, $pos + 1); + } + /** * Get the absolute media path * diff --git a/Models/MediaContent.php b/Models/MediaContent.php index f2ea857..f945d32 100755 --- a/Models/MediaContent.php +++ b/Models/MediaContent.php @@ -30,7 +30,7 @@ class MediaContent implements \JsonSerializable * @var int * @since 1.0.0 */ - protected int $id = 0; + public int $id = 0; public string $content = ''; diff --git a/Models/MediaMapper.php b/Models/MediaMapper.php index ba61f52..ddaf4f5 100755 --- a/Models/MediaMapper.php +++ b/Models/MediaMapper.php @@ -49,7 +49,7 @@ class MediaMapper extends DataMapperFactory 'media_file' => ['name' => 'media_file', 'type' => 'string', 'internal' => 'path', 'autocomplete' => true], 'media_virtual' => ['name' => 'media_virtual', 'type' => 'string', 'internal' => 'virtualPath', 'autocomplete' => true], 'media_absolute' => ['name' => 'media_absolute', 'type' => 'bool', 'internal' => 'isAbsolute'], - 'media_nonce' => ['name' => 'media_nonce', 'type' => 'string', 'internal' => 'nonce'], + 'media_encrypted' => ['name' => 'media_encrypted', 'type' => 'bool', 'internal' => 'isEncrypted'], 'media_password' => ['name' => 'media_password', 'type' => 'string', 'internal' => 'password'], 'media_extension' => ['name' => 'media_extension', 'type' => 'string', 'internal' => 'extension'], 'media_size' => ['name' => 'media_size', 'type' => 'int', 'internal' => 'size'], diff --git a/Models/MediaType.php b/Models/MediaType.php index a290c59..684828d 100755 --- a/Models/MediaType.php +++ b/Models/MediaType.php @@ -33,7 +33,7 @@ class MediaType implements \JsonSerializable * @var int * @since 1.0.0 */ - protected int $id = 0; + public int $id = 0; /** * Name. diff --git a/Models/UploadFile.php b/Models/UploadFile.php index 7bd5502..3a846ca 100755 --- a/Models/UploadFile.php +++ b/Models/UploadFile.php @@ -17,6 +17,7 @@ declare(strict_types=1); namespace Modules\Media\Models; use phpOMS\Log\FileLogger; +use phpOMS\Security\EncryptionHelper; use phpOMS\System\File\Local\Directory; use phpOMS\System\File\Local\File; @@ -202,29 +203,13 @@ class UploadFile } if ($encryptionKey !== '') { - $nonce = \sodium_randombytes_buf(24); + $isEncrypted = EncryptionHelper::encryptFile($dest, $dest, $encryptionKey); - $fpSource = \fopen($dest, 'r+'); - $fpEncoded = \fopen($dest . '.tmp', 'w'); - - if ($fpSource === false || $fpEncoded === false) { + if (!$isEncrypted) { $result[$key]['status'] = UploadStatus::NOT_ENCRYPTABLE; return $result; } - - while (($buffer = \fgets($fpSource, 4096)) !== false) { - $encrypted = \sodium_crypto_secretbox($buffer, $nonce, $encryptionKey); - - \fwrite($fpEncoded, $encrypted); - } - - \fclose($fpSource); - \fclose($fpEncoded); - - \unlink($dest); - \rename($dest . '.tmp', $dest); - $result[$key]['nonce'] = $nonce; } /* diff --git a/Theme/Backend/Components/InlinePreview/BaseView.php b/Theme/Backend/Components/InlinePreview/BaseView.php index 041e088..6ad0516 100755 --- a/Theme/Backend/Components/InlinePreview/BaseView.php +++ b/Theme/Backend/Components/InlinePreview/BaseView.php @@ -68,7 +68,7 @@ class BaseView extends View * @var bool * @since 1.0.0 */ - private bool $isRequired = false; + public bool $isRequired = false; /** * {@inheritdoc} diff --git a/Theme/Backend/Components/InlinePreview/inline-preview.tpl.php b/Theme/Backend/Components/InlinePreview/inline-preview.tpl.php index b28d136..6eb154e 100755 --- a/Theme/Backend/Components/InlinePreview/inline-preview.tpl.php +++ b/Theme/Backend/Components/InlinePreview/inline-preview.tpl.php @@ -1,10 +1,10 @@
- -
- + -