Ctrl K
ComunidadGuías…

engine/classes/ElggFile.php

Rama 7.x · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<?php

use Elgg\Exceptions\Filesystem\IOException;
use Elgg\Exceptions\DomainException as ElggDomainException;
use Elgg\Exceptions\InvalidArgumentException as ElggInvalidArgumentException;
use Elgg\Project\Paths;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
 * This class represents a physical file.
 *
 * Create a new \ElggFile object and specify a filename
 *
 * Open the file using the appropriate mode, and you will be able to
 * read and write to the file.
 *
 * Optionally, you can also call the file's save() method, this will
 * turn the file into an entity in the system and permit you to do
 * things like attach tags to the file. If you do not save the file, no
 * entity is created in the database. This is because there are occasions
 * when you may want access to file data on datastores using the \ElggFile
 * interface without a need to persist information such as temporary files.
 *
 * @property      string $mimetype         MIME type of the file
 * @property      string $simpletype       Category of the file
 * @property      string $originalfilename Filename of the original upload
 * @property      int    $upload_time      Timestamp of the upload action, used as a filename prefix
 * @property      string $filestore_prefix Prefix (directory) on user's filestore where the file is saved
 * @property-read string $filename         The filename of the file
 */
class ElggFile extends ElggObject {

	/**
	 * @var resource|null|false File handle used to identify this file in a filestore
	 * @see \ElggFile::open()
	 */
	private $handle;

	/**
	 * Set subtype to 'file'.
	 *
	 * @return void
	 */
	protected function initializeAttributes() {
		parent::initializeAttributes();

		$this->attributes['subtype'] = 'file';
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function __set($name, $value) {
		switch ($name) {
			case 'filename':
				// ensure sanitization
				$this->setFilename($value);
				return;
		}
		
		parent::__set($name, $value);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public function __get($name) {
		switch ($name) {
			case 'filename':
				// ensure sanitization
				return $this->getFilename();
		}
		
		return parent::__get($name);
	}

	/**
	 * Set the filename of this file. This filename will be sanitized to prevent path traversal
	 *
	 * @param string $filename The filename
	 *
	 * @return void
	 */
	public function setFilename(string $filename): void {
		$filename = ltrim(Paths::sanitize($filename, false), '/');
		
		parent::__set('filename', $filename);
	}

	/**
	 * Return the filename. This filename will be sanitized to prevent path traversal
	 *
	 * @return string
	 */
	public function getFilename(): string {
		$filename = parent::__get('filename');
		if (empty($filename)) {
			return '';
		}
		
		return ltrim(Paths::sanitize($filename, false), '/');
	}

	/**
	 * Return the filename of this file as it is/will be stored on the
	 * filestore, which may be different to the filename.
	 *
	 * @return string
	 */
	public function getFilenameOnFilestore(): string {
		return $this->getFilestore()->getFilenameOnFilestore($this);
	}

	/**
	 * Get the mime type of the file.
	 * Returns mimetype metadata value if set, otherwise attempts to detect it.
	 *
	 * @return string|false
	 */
	public function getMimeType(): string|false {
		if ($this->mimetype) {
			return $this->mimetype;
		}
		
		try {
			return _elgg_services()->mimetype->getMimeType($this->getFilenameOnFilestore());
		} catch (ElggInvalidArgumentException $e) {
			// the file has no file on the filesystem
			// can happen in tests etc.
		}
		
		return false;
	}

	/**
	 * Set the mime type of the file.
	 *
	 * @param string $mimetype The mimetype
	 *
	 * @return void
	 */
	public function setMimeType(string $mimetype): void {
		$this->mimetype = $mimetype;
	}

	/**
	 * Get the simple type of the file.
	 * Returns simpletype metadata value if set, otherwise parses it from mimetype
	 *
	 * @return string 'document', 'audio', 'video', or 'general' if the MIME type was unrecognized
	 */
	public function getSimpleType(): string {
		if (isset($this->simpletype)) {
			return $this->simpletype;
		}
		
		return _elgg_services()->mimetype->getSimpleType($this->getMimeType() ?: '');
	}

	/**
	 * Open the file with the given mode
	 *
	 * @param string $mode Either read/write/append
	 *
	 * @return false|resource File handler
	 *
	 * @throws IOException
	 * @throws \Elgg\Exceptions\DomainException
	 */
	public function open(string $mode) {
		if (!$this->getFilename()) {
			throw new IOException('You must specify a name before opening a file.');
		}

		if (!in_array($mode, ['read', 'write', 'append'])) {
			throw new ElggDomainException("Unrecognized file mode '{$mode}'");
		}

		// Open the file handle
		$this->handle = $this->getFilestore()->open($this, $mode);

		return $this->handle;
	}

	/**
	 * Write data.
	 *
	 * @param string $data The data
	 *
	 * @return false|int
	 */
	public function write(string $data): int|false {
		return $this->getFilestore()->write($this->handle, $data);
	}

	/**
	 * Read data.
	 *
	 * @param int $length Amount to read.
	 * @param int $offset The offset to start from.
	 *
	 * @return mixed Data or false
	 */
	public function read(int $length, int $offset = 0) {
		return $this->getFilestore()->read($this->handle, $length, $offset);
	}

	/**
	 * Gets the full contents of this file.
	 *
	 * @return false|string The file contents.
	 */
	public function grabFile(): string|false {
		return $this->getFilestore()->grabFile($this);
	}

	/**
	 * Close the file and commit changes
	 *
	 * @return bool
	 */
	public function close(): bool {
		if (is_resource($this->handle) && $this->getFilestore()->close($this->handle)) {
			$this->handle = null;

			return true;
		}

		return false;
	}

	/**
	 * {@inheritdoc}
	 */
	public function delete(bool $recursive = true, ?bool $persistent = null): bool {
		if (!$this->guid) {
			return $this->persistentDelete($recursive);
		}
		
		return parent::delete($recursive, $persistent);
	}
	
	/**
	 * {@inheritdoc}
	 */
	protected function persistentDelete(bool $recursive = true): bool {
		if ($this->guid) {
			$result = parent::persistentDelete($recursive);
			if ($result) {
				$this->getFilestore()->delete($this);
			}
			
			return $result;
		}
		
		return $this->getFilestore()->delete($this);
	}

	/**
	 * Seek a position in the file.
	 *
	 * @param int $position Position in bytes
	 *
	 * @return void
	 */
	public function seek(int $position): void {
		$this->getFilestore()->seek($this->handle, $position);
	}

	/**
	 * Return the current position of the file.
	 *
	 * @return int The file position
	 */
	public function tell(): int {
		return $this->getFilestore()->tell($this->handle);
	}

	/**
	 * Updates modification time of the file and clears stats cache for the file
	 *
	 * @return bool
	 */
	public function setModifiedTime(): bool {
		$filestorename = $this->getFilenameOnFilestore();
		
		$modified = touch($filestorename);
		if ($modified) {
			clearstatcache(true, $filestorename);
		} else {
			elgg_log("Unable to update modified time for {$filestorename}", \Psr\Log\LogLevel::ERROR);
		}
		
		return $modified;
	}

	/**
	 * Returns file modification time
	 *
	 * @return int
	 */
	public function getModifiedTime(): int {
		return filemtime($this->getFilenameOnFilestore());
	}

	/**
	 * Return the size of the file in bytes.
	 *
	 * @return int
	 * @since 1.9
	 */
	public function getSize(): int {
		return $this->getFilestore()->getFileSize($this);
	}

	/**
	 * Return a boolean value whether the file handle is at the end of the file
	 *
	 * @return bool
	 */
	public function eof(): bool {
		return $this->getFilestore()->eof($this->handle);
	}

	/**
	 * Returns if the file exists
	 *
	 * @return bool
	 */
	public function exists(): bool {
		return $this->getFilestore()->exists($this);
	}

	/**
	 * Return the system filestore based on dataroot.
	 *
	 * @return \Elgg\Filesystem\Filestore\DiskFilestore
	 */
	protected function getFilestore(): \Elgg\Filesystem\Filestore\DiskFilestore {
		return _elgg_services()->filestore;
	}

	/**
	 * Transfer a file to a new owner and sets a new filename,
	 * copies file contents to a new location.
	 *
	 * This is an alternative to using rename() which fails to move files to
	 * a non-existent directory under new owner's filestore directory
	 *
	 * @param int         $owner_guid New owner's guid
	 * @param null|string $filename   New filename (uses old filename if not set)
	 *
	 * @return bool
	 */
	public function transfer(int $owner_guid, ?string $filename = null): bool {
		if ($owner_guid < 1) {
			return false;
		}

		if (!$this->exists()) {
			return false;
		}

		if (empty($filename)) {
			$filename = $this->getFilename();
		}
		
		$filestorename = $this->getFilenameOnFilestore();

		$this->owner_guid = $owner_guid;
		$this->setFilename($filename);
		$this->open('write');
		$this->close();

		return rename($filestorename, $this->getFilenameOnFilestore());
	}

	/**
	 * Writes contents of the uploaded file to an instance of ElggFile
	 *
	 * @note Note that this function moves the file and populates properties,
	 * but does not call ElggFile::save().
	 *
	 * @note This method will automatically assign a filename on filestore based
	 * on the upload time and filename. By default, the file will be written
	 * to /file directory on owner's filestore. You can change this directory,
	 * by setting 'filestore_prefix' property of the ElggFile instance before
	 * calling this method.
	 *
	 * @param UploadedFile $upload Uploaded file object
	 *
	 * @return bool
	 */
	public function acceptUploadedFile(UploadedFile $upload): bool {
		if (!$upload->isValid()) {
			return false;
		}

		$old_filestorename = '';
		if ($this->exists()) {
			$old_filestorename = $this->getFilenameOnFilestore();
		}

		$originalfilename = $upload->getClientOriginalName();
		$this->originalfilename = $originalfilename;
		if (empty($this->title)) {
			$this->title = htmlspecialchars($this->originalfilename, ENT_QUOTES, 'UTF-8');
		}

		$this->upload_time = time();
		$prefix = $this->filestore_prefix ?: 'file';
		$prefix = trim($prefix, '/');
		$filename = elgg_strtolower("{$prefix}/{$this->upload_time}{$this->originalfilename}");
		$this->setFilename($filename);
		$this->filestore_prefix = $prefix;

		$params = [
			'file' => $this,
			'upload' => $upload,
		];

		$uploaded = _elgg_services()->events->triggerResults('upload', 'file', $params);
		if ($uploaded !== true && $uploaded !== false) {
			$filestorename = $this->getFilenameOnFilestore();
			try {
				$uploaded = $upload->move(pathinfo($filestorename, PATHINFO_DIRNAME), pathinfo($filestorename, PATHINFO_BASENAME));
			} catch (FileException $ex) {
				_elgg_services()->logger->error($ex->getMessage());
				$uploaded = false;
			}
		}

		if ($uploaded) {
			if ($old_filestorename && $old_filestorename != $this->getFilenameOnFilestore()) {
				// remove old file
				unlink($old_filestorename);
			}
			
			try {
				// try to detect mimetype
				$mime_type = _elgg_services()->mimetype->getMimeType($this->getFilenameOnFilestore());
				$this->setMimeType($mime_type);
				$this->simpletype = _elgg_services()->mimetype->getSimpleType($mime_type);
			} catch (ElggInvalidArgumentException $e) {
				// this can fail if the upload events returns true, but the file is not present on the filestore
				// this happens in a unittest
			}
			
			_elgg_services()->events->triggerAfter('upload', 'file', $this);
			return true;
		}

		return false;
	}

	/**
	 * Get property names to serialize.
	 *
	 * @return string[]
	 */
	public function __sleep() {
		return array_diff(array_keys(get_object_vars($this)), [
			// a resource
			'handle',
		]);
	}

	/**
	 * Checks the download permissions for the file
	 *
	 * @param int  $user_guid GUID of the user (defaults to logged in user)
	 * @param bool $default   Default permission
	 *
	 * @return bool
	 */
	public function canDownload(int $user_guid = 0, bool $default = true): bool {
		return _elgg_services()->userCapabilities->canDownload($this, $user_guid, $default);
	}

	/**
	 * Returns file's download URL
	 *
	 * @note This does not work for files with custom filestores.
	 *
	 * @param bool   $use_cookie Limit URL validity to current session only
	 * @param string $expires    URL expiration, as a string suitable for strtotime()
	 *
	 * @return string|null
	 */
	public function getDownloadURL(bool $use_cookie = true, string $expires = '+2 hours'): ?string {
		$file_svc = new \Elgg\FileService\File();
		$file_svc->setFile($this);
		if (!empty($expires)) {
			$file_svc->setExpires($expires);
		}
		
		$file_svc->setDisposition('attachment');
		$file_svc->bindSession($use_cookie);

		$params = [
			'entity' => $this,
			'use_cookie' => $use_cookie,
			'expires' => $expires,
		];
		return _elgg_services()->events->triggerResults('download:url', 'file', $params, $file_svc->getURL());
	}

	/**
	 * Returns file's URL for inline display
	 * Suitable for displaying cacheable resources, such as user avatars
	 *
	 * @note This does not work for files with custom filestores.
	 *
	 * @param bool   $use_cookie Limit URL validity to current session only
	 * @param string $expires    URL expiration, as a string suitable for strtotime()
	 *
	 * @return string|null
	 */
	public function getInlineURL(bool $use_cookie = false, string $expires = ''): ?string {
		$file_svc = new \Elgg\FileService\File();
		$file_svc->setFile($this);
		if (!empty($expires)) {
			$file_svc->setExpires($expires);
		}
		
		$file_svc->setDisposition('inline');
		$file_svc->bindSession($use_cookie);

		$params = [
			'entity' => $this,
			'use_cookie' => $use_cookie,
			'expires' => $expires,
		];
		return _elgg_services()->events->triggerResults('inline:url', 'file', $params, $file_svc->getURL());
	}
}