mirror of
https://github.com/humhub/humhub.git
synced 2025-03-15 04:29:45 +01:00
547 lines
15 KiB
PHP
547 lines
15 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @link https://www.humhub.org/
|
|
* @copyright Copyright (c) 2016 HumHub GmbH & Co. KG
|
|
* @license https://www.humhub.com/licences
|
|
*/
|
|
|
|
namespace humhub\modules\file\models;
|
|
|
|
use humhub\components\ActiveRecord;
|
|
use humhub\components\behaviors\GUID;
|
|
use humhub\components\behaviors\PolymorphicRelation;
|
|
use humhub\interfaces\ViewableInterface;
|
|
use humhub\libs\StdClass;
|
|
use humhub\libs\UUIDValidator;
|
|
use humhub\modules\content\components\ContentActiveRecord;
|
|
use humhub\modules\content\components\ContentAddonActiveRecord;
|
|
use humhub\modules\file\components\StorageManager;
|
|
use humhub\modules\file\components\StorageManagerInterface;
|
|
use humhub\modules\file\libs\Metadata;
|
|
use Throwable;
|
|
use Yii;
|
|
use yii\base\InvalidArgumentException;
|
|
use yii\base\InvalidConfigException;
|
|
use yii\db\ActiveQuery;
|
|
use yii\db\Exception;
|
|
use yii\db\IntegrityException;
|
|
use yii\db\StaleObjectException;
|
|
use yii\helpers\Url;
|
|
use yii\web\UploadedFile;
|
|
|
|
/**
|
|
* This is the model class for table "file".
|
|
*
|
|
* The following are the available columns in table 'file':
|
|
*
|
|
* @property integer $id
|
|
* @property string $guid
|
|
* @property integer $state
|
|
* @property integer|null $category Note, categories are still experimental. Expect changes in v1.16 (ToDo)
|
|
* @property string $file_name
|
|
* @property string $title
|
|
* @property string $mime_type
|
|
* @property string $size
|
|
* @property-read Metadata $metadata since 1.15. Note, $metadata is still experimental. Expect changes in v1.16 (ToDo).
|
|
* This property is read-only in the sense that no new instance be assigned to the model.
|
|
* Edit data always by working on the object itself.
|
|
* You best retrieve is using `static::getMetadata()`.
|
|
* E.g, to set a value you could do:
|
|
* ```
|
|
* // setting a single value
|
|
* $model->getMetadata()->property1 = "some value";
|
|
* // or
|
|
* $model->getMetadata()['property2'] = "some other value";
|
|
*
|
|
* // setting multiple values
|
|
* $metadata = $model->getMetadata();
|
|
* $metadata->property1 = "some value";
|
|
* $metadata['property2'] = "some other value";
|
|
*
|
|
* // alternatively, the `Metadata::addValues()` method can be used:
|
|
* $model->getMetadata()->addValues(['property1' => "some value", 'property2' => "some other value"] = "some other value";
|
|
* ```
|
|
*
|
|
* @property string|null $object_model
|
|
* @property integer|null $object_id
|
|
* @property integer|null $content_id
|
|
* @property integer $sort_order
|
|
* @property string|null $created_at
|
|
* @property integer|null $created_by
|
|
* @property string|null $updated_at
|
|
* @property integer|null $updated_by
|
|
* @property integer|null $show_in_stream
|
|
* @property string|null $hash_sha1
|
|
*
|
|
* @property StorageManager $store
|
|
* @property FileHistory[] $historyFiles
|
|
*
|
|
* @mixin PolymorphicRelation
|
|
* @mixin GUID
|
|
*
|
|
* Following properties are optional and for module-dependent use:
|
|
* - title
|
|
*
|
|
* @since 0.5
|
|
* @noinspection PropertiesInspection
|
|
*/
|
|
class File extends FileCompat implements ViewableInterface
|
|
{
|
|
/**
|
|
* @event Event that is triggered after a new file content has been stored.
|
|
*/
|
|
public const EVENT_AFTER_NEW_STORED_FILE = 'afterNewStoredFile';
|
|
|
|
/**
|
|
* The numeric value of the published state is not yet finalized. Use with caution and expect a change of value in later versions.
|
|
*
|
|
* @deprecated since 1.15
|
|
* @since 1.15
|
|
*/
|
|
public const STATE_PUBLISHED = 1;
|
|
|
|
/**
|
|
* @var int $old_updated_by
|
|
*/
|
|
public $old_updated_by;
|
|
|
|
/**
|
|
* @var string $old_updated_at
|
|
*/
|
|
public $old_updated_at;
|
|
|
|
/**
|
|
* @var StorageManagerInterface the storage manager
|
|
*
|
|
* @codingStandardsIgnoreStart PSR2.Methods.MethodDeclaration.Underscore
|
|
**/
|
|
private $_store = null;
|
|
/* @codingStandardsIgnoreEnd PSR2.Methods.MethodDeclaration.Underscore */
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public static function tableName()
|
|
{
|
|
return 'file';
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function rules()
|
|
{
|
|
return [
|
|
[['mime_type'], 'string', 'max' => 150],
|
|
[
|
|
['mime_type'],
|
|
'match',
|
|
'not' => true,
|
|
'pattern' => '/[^a-zA-Z0-9\.ä\/\-\+]/',
|
|
'message' => Yii::t('FileModule.base', 'Invalid Mime-Type')
|
|
],
|
|
[['category', 'size', 'state', 'sort_order'], 'integer'],
|
|
[['file_name', 'title'], 'string', 'max' => 255],
|
|
[['guid'], UUIDValidator::class],
|
|
[['guid'], 'unique'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function behaviors()
|
|
{
|
|
return [
|
|
[
|
|
'class' => PolymorphicRelation::class,
|
|
'mustBeInstanceOf' => [ActiveRecord::class],
|
|
],
|
|
[
|
|
'class' => GUID::class,
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
* @noinspection PhpMissingReturnTypeInspection
|
|
* @noinspection ReturnTypeCanBeDeclaredInspection
|
|
*/
|
|
public function transactions()
|
|
{
|
|
return [
|
|
// ToDo: enable in v.16
|
|
// 'default' => self::OP_INSERT + self::OP_DELETE,
|
|
];
|
|
}
|
|
|
|
public function __get($name)
|
|
{
|
|
if ($name === 'metadata') {
|
|
return $this->getMetadata();
|
|
}
|
|
|
|
return parent::__get($name);
|
|
}
|
|
|
|
/**
|
|
* Gets a query for [[FileHistory]].
|
|
*
|
|
* @return ActiveQuery
|
|
*/
|
|
public function getHistoryFiles()
|
|
{
|
|
/** @noinspection MissedFieldInspection */
|
|
return $this->hasMany(FileHistory::class, ['file_id' => 'id'])->orderBy(['created_at' => SORT_DESC, 'id' => SORT_DESC]);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function beforeSave($insert)
|
|
{
|
|
/**
|
|
* Used for file history
|
|
*
|
|
* @see FileHistory::createEntryForFile()
|
|
*/
|
|
$this->old_updated_by = $this->getOldAttribute('updated_by');
|
|
$this->old_updated_at = $this->getOldAttribute('updated_at');
|
|
|
|
$this->state ??= self::STATE_PUBLISHED;
|
|
|
|
$this->sort_order ??= 0;
|
|
|
|
$metadata = $this->getAttribute('metadata');
|
|
|
|
if (($metadata instanceof StdClass) && !$metadata->isModified()) {
|
|
$this->setAttribute('metadata', null);
|
|
}
|
|
|
|
return parent::beforeSave($insert);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function beforeDelete()
|
|
{
|
|
$this->getStore()->delete();
|
|
FileHistory::deleteAll(['file_id' => $this->id]);
|
|
|
|
return parent::beforeDelete();
|
|
}
|
|
|
|
/**
|
|
* Returns the url to this file
|
|
*
|
|
* Available params (see also: DownloadAction)
|
|
* - variant: the requested file variant
|
|
* - download: force download option (default: false)
|
|
*
|
|
* @param array $params the params
|
|
* @param boolean $absolute
|
|
* @return string the url to the file download
|
|
*/
|
|
public function getUrl($params = [], $absolute = true)
|
|
{
|
|
// Handle old 'suffix' attribute for HumHub prior 1.1 versions
|
|
if (is_string($params)) {
|
|
$suffix = $params;
|
|
$params = [];
|
|
if ($suffix != '') {
|
|
$params['variant'] = $suffix;
|
|
}
|
|
}
|
|
|
|
$params['guid'] = $this->guid;
|
|
$params['hash_sha1'] = $this->getHash(8);
|
|
array_unshift($params, '/file/file/download');
|
|
return Url::to($params, $absolute);
|
|
}
|
|
|
|
/**
|
|
* Get hash
|
|
*
|
|
* @param int $length Return number of first chars of the file hash, 0 - unlimited
|
|
*
|
|
* @return string
|
|
* @throws InvalidConfigException
|
|
*/
|
|
public function getHash($length = 0)
|
|
{
|
|
$store = $this->getStore();
|
|
|
|
if (empty($this->hash_sha1) && $store->has()) {
|
|
$this->updateAttributes(['hash_sha1' => sha1_file($store->get())]);
|
|
}
|
|
|
|
return $length
|
|
? substr($this->hash_sha1, 0, $length)
|
|
: $this->hash_sha1;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use canView() instead. It will be deleted since v1.17
|
|
*/
|
|
public function canRead($user = null): bool
|
|
{
|
|
return $this->canView($user);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function canView($user = null): bool
|
|
{
|
|
$object = $this->getPolymorphicRelation();
|
|
if ($object instanceof ContentActiveRecord || $object instanceof ContentAddonActiveRecord) {
|
|
return $object->content->canView($user);
|
|
}
|
|
if ($object instanceof ViewableInterface) {
|
|
return $object->canView($user);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks if given file can be deleted.
|
|
*
|
|
* If the file is not an instance of ContentActiveRecord or ContentAddonActiveRecord
|
|
* the file is readable for all unless there is method canEdit or canDelete implemented.
|
|
*
|
|
* @param null $userId
|
|
*
|
|
* @return bool
|
|
* @throws IntegrityException
|
|
* @throws InvalidConfigException
|
|
* @throws Throwable
|
|
* @throws \yii\base\Exception
|
|
*/
|
|
public function canDelete($userId = null): bool
|
|
{
|
|
$object = $this->getPolymorphicRelation();
|
|
|
|
// File is not bound to an object
|
|
if ($object === null) {
|
|
return true;
|
|
}
|
|
|
|
if ($object instanceof ContentAddonActiveRecord) {
|
|
/** @var ContentAddonActiveRecord $object */
|
|
return $object->canEdit($userId) || $object->content->canEdit($userId);
|
|
}
|
|
|
|
if ($object instanceof ContentActiveRecord) {
|
|
/** @var ContentActiveRecord $object */
|
|
return $object->content->canEdit($userId);
|
|
}
|
|
|
|
if ($object instanceof ActiveRecord && method_exists($object, 'canEdit')) {
|
|
/** @var ActiveRecord $object */
|
|
return $object->canEdit($userId);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks if this file record is already attached to record.
|
|
*
|
|
* @return boolean is whether in use or not
|
|
*/
|
|
public function isAssigned()
|
|
{
|
|
return (!empty($this->object_model));
|
|
}
|
|
|
|
/**
|
|
* Checks if this file is attached to the given record
|
|
*
|
|
* @param ActiveRecord $record
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isAssignedTo(ActiveRecord $record)
|
|
{
|
|
return $this->object_model === get_class($record) && $this->object_id == $record->getPrimaryKey();
|
|
}
|
|
|
|
/**
|
|
* @return Metadata
|
|
*/
|
|
public function getMetadata(): Metadata
|
|
{
|
|
/** @var Metadata|null $metadata */
|
|
$metadata = $this->getAttribute('metadata');
|
|
|
|
if ($metadata instanceof Metadata) {
|
|
return $metadata;
|
|
}
|
|
|
|
$metadata = new Metadata($metadata);
|
|
|
|
$this->setAttribute('metadata', $metadata);
|
|
|
|
return $metadata;
|
|
}
|
|
|
|
/**
|
|
* @param string|array $metadata
|
|
*
|
|
* @return File
|
|
*/
|
|
public function setMetadata($metadata): File
|
|
{
|
|
/** @var Metadata|null $md */
|
|
$md = $this->metadata;
|
|
|
|
$md->addValues($metadata);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Returns the StorageManager
|
|
*
|
|
* @return StorageManagerInterface
|
|
* @throws InvalidConfigException
|
|
*/
|
|
public function getStore()
|
|
{
|
|
if ($this->_store === null) {
|
|
$this->_store = Yii::createObject(Yii::$app->getModule('file')->storageManagerClass);
|
|
$this->_store->setFile($this);
|
|
}
|
|
|
|
return $this->_store;
|
|
}
|
|
|
|
/**
|
|
* Returns all attached Files of the given $record.
|
|
*
|
|
* @param ActiveRecord $record
|
|
* @return File[]
|
|
*/
|
|
public static function findByRecord(ActiveRecord $record): array
|
|
{
|
|
return self::findAll(['object_model' => PolymorphicRelation::getObjectModel($record), 'object_id' => $record->getPrimaryKey()]);
|
|
}
|
|
|
|
/**
|
|
* Get File History by ID
|
|
*
|
|
* @param int $fileHistoryId
|
|
*
|
|
* @return FileHistory|null
|
|
*/
|
|
public function getFileHistoryById($fileHistoryId): ?FileHistory
|
|
{
|
|
if (empty($fileHistoryId) || $this->isNewRecord) {
|
|
return null;
|
|
}
|
|
|
|
return FileHistory::findOne(['id' => $fileHistoryId, 'file_id' => $this->id]);
|
|
}
|
|
|
|
/**
|
|
* Sets a new file content based on an UploadedFile, new File or a file path.
|
|
*
|
|
* @param UploadedFile|File|string $file File object or path
|
|
* @param bool $skipHistoryEntry Skipping the creation of a history entry, even if enabled by the record
|
|
*
|
|
* @throws Exception
|
|
* @throws IntegrityException
|
|
* @throws InvalidConfigException
|
|
* @throws Throwable
|
|
* @throws StaleObjectException
|
|
* @since 1.10
|
|
*/
|
|
public function setStoredFile($file, $skipHistoryEntry = false)
|
|
{
|
|
$this->beforeNewStoredFile($skipHistoryEntry);
|
|
|
|
$store = $this->getStore();
|
|
|
|
if ($file instanceof UploadedFile) {
|
|
$store->set($file);
|
|
} elseif ($file instanceof self) {
|
|
if ($file->isAssigned()) {
|
|
throw new InvalidArgumentException('Already assigned File records cannot stored as another File record.');
|
|
}
|
|
$store->setByPath($file->getStore()->get());
|
|
$file->delete();
|
|
} elseif (is_string($file) && is_file($file)) {
|
|
$store->setByPath($file);
|
|
}
|
|
|
|
$this->afterNewStoredFile();
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets a new file content by a given string.
|
|
*
|
|
* @param string $content
|
|
* @param bool $skipHistoryEntry Skipping the creation of a history entry, even if enabled by the record
|
|
*
|
|
* @throws Exception
|
|
* @throws IntegrityException
|
|
* @throws \yii\base\Exception
|
|
* @since 1.10
|
|
*/
|
|
public function setStoredFileContent($content, $skipHistoryEntry = false)
|
|
{
|
|
$this->beforeNewStoredFile($skipHistoryEntry);
|
|
$this->getStore()->setContent($content);
|
|
$this->afterNewStoredFile();
|
|
}
|
|
|
|
/**
|
|
* Steps that must be executed before a new file content is set.
|
|
*
|
|
* @param bool $skipHistoryEntry Skipping the creation of a history entry, even if enabled by the record
|
|
*
|
|
* @throws Exception|IntegrityException|InvalidConfigException
|
|
*/
|
|
private function beforeNewStoredFile(bool $skipHistoryEntry)
|
|
{
|
|
if ($this->isNewRecord) {
|
|
throw new Exception('File Record must be saved before setting a new file content.');
|
|
}
|
|
|
|
$store = $this->getStore();
|
|
|
|
if ($store->has() && FileHistory::isEnabled($this) && !$skipHistoryEntry) {
|
|
FileHistory::createEntryForFile($this);
|
|
}
|
|
|
|
$store->delete(null, ['file', FileHistory::VARIANT_PREFIX . '*']);
|
|
}
|
|
|
|
/**
|
|
* Steps that must be performed after a new file content has been set.
|
|
*/
|
|
private function afterNewStoredFile()
|
|
{
|
|
$store = $this->getStore();
|
|
|
|
if ($store->has()) {
|
|
// Make sure to update updated_by & updated_at and avoid save()
|
|
$this->beforeSave(false);
|
|
|
|
$filename = $store->get();
|
|
$this->updateAttributes([
|
|
'hash_sha1' => sha1_file($filename),
|
|
'size' => filesize($filename),
|
|
'updated_by' => $this->updated_by,
|
|
'updated_at' => $this->updated_at,
|
|
]);
|
|
$this->trigger(self::EVENT_AFTER_NEW_STORED_FILE);
|
|
}
|
|
}
|
|
}
|