2021-04-25 22:25:56 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
// {{{ License
|
|
|
|
// This file is part of GNU social - https://www.gnu.org/software/social
|
|
|
|
//
|
|
|
|
// GNU social is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// GNU social is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
// }}}
|
|
|
|
|
|
|
|
namespace App\Util;
|
|
|
|
|
2021-09-06 19:49:03 +01:00
|
|
|
use function App\Core\I18n\_m;
|
2021-04-29 21:27:41 +01:00
|
|
|
use App\Util\Exception\TemporaryFileException;
|
2021-07-20 21:17:53 +01:00
|
|
|
use Symfony\Component\Mime\MimeTypes;
|
2021-04-29 21:27:41 +01:00
|
|
|
|
2021-04-25 22:25:56 +01:00
|
|
|
/**
|
|
|
|
* Class oriented at providing automatic temporary file handling.
|
|
|
|
*
|
|
|
|
* @package GNUsocial
|
|
|
|
*
|
|
|
|
* @author Alexei Sorokin <sor.alexei@meowr.ru>
|
2021-07-20 21:17:53 +01:00
|
|
|
* @author Hugo Sales <hugo@hsal.es>
|
|
|
|
* @author Diogo Peralta Cordeiro <mail@diogo.site>
|
|
|
|
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
|
2021-04-25 22:25:56 +01:00
|
|
|
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
|
|
|
*/
|
|
|
|
class TemporaryFile extends \SplFileInfo
|
|
|
|
{
|
2021-07-28 22:16:18 +01:00
|
|
|
// Cannot type annotate currently. `resource` is the expected type, but it's not a builtin type
|
2021-04-25 22:25:56 +01:00
|
|
|
protected $resource;
|
|
|
|
|
|
|
|
/**
|
2021-07-28 22:16:18 +01:00
|
|
|
* @param array $options - ['prefix' => ?string, 'suffix' => ?string, 'mode' => ?string, 'directory' => ?string, 'attempts' => ?int]
|
2021-07-20 21:17:53 +01:00
|
|
|
* Description of options:
|
|
|
|
* > prefix: The file name will begin with that prefix, default is 'gs-php'
|
2021-07-22 20:56:29 +01:00
|
|
|
* > suffix: The file name will end with that suffix, default is ''
|
|
|
|
* > mode: Operation mode, default is 'w+b'
|
|
|
|
* > directory: Directory where the file will be used, default is the system's temporary
|
2021-07-28 22:16:18 +01:00
|
|
|
* > attempts: Default 16, how many times to attempt to find a unique file
|
2021-07-20 21:17:53 +01:00
|
|
|
*
|
|
|
|
* @throws TemporaryFileException
|
2021-04-25 22:25:56 +01:00
|
|
|
*/
|
2021-05-05 13:44:07 +01:00
|
|
|
public function __construct(array $options = [])
|
2021-05-02 16:02:26 +01:00
|
|
|
{
|
2021-07-28 22:16:18 +01:00
|
|
|
// todo options permission
|
|
|
|
$attempts = $options['attempts'] ?? 16;
|
|
|
|
$filepath = uniqid(($options['directory'] ?? (sys_get_temp_dir() . '/')) . ($options['prefix'] ?? 'gs-php')) . ($options['suffix'] ?? '');
|
2021-05-02 16:02:26 +01:00
|
|
|
for ($count = 0; $count < $attempts; ++$count) {
|
2021-07-28 22:16:18 +01:00
|
|
|
$this->resource = @fopen($filepath, $options['mode'] ?? 'w+b');
|
2021-05-02 16:02:26 +01:00
|
|
|
if ($this->resource !== false) {
|
|
|
|
break;
|
|
|
|
}
|
2021-04-25 22:25:56 +01:00
|
|
|
}
|
2021-07-28 22:16:18 +01:00
|
|
|
if ($this->resource === false) {
|
2021-08-08 02:05:45 +01:00
|
|
|
// @codeCoverageIgnoreStart
|
2021-04-25 22:25:56 +01:00
|
|
|
$this->cleanup();
|
2021-07-28 22:16:18 +01:00
|
|
|
throw new TemporaryFileException('Could not open file: ' . $filepath);
|
2021-08-08 02:05:45 +01:00
|
|
|
// @codeCoverageIgnoreEnd
|
2021-04-25 22:25:56 +01:00
|
|
|
}
|
2021-05-02 16:02:26 +01:00
|
|
|
|
2021-07-28 22:16:18 +01:00
|
|
|
parent::__construct($filepath);
|
2021-04-25 22:25:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function __destruct()
|
|
|
|
{
|
|
|
|
$this->close();
|
|
|
|
$this->cleanup();
|
|
|
|
}
|
|
|
|
|
2021-07-20 21:17:53 +01:00
|
|
|
/**
|
|
|
|
* Binary-safe file write
|
|
|
|
*
|
|
|
|
* @see https://php.net/manual/en/function.fwrite.php
|
|
|
|
*
|
|
|
|
* @param string $data The string that is to be written.
|
|
|
|
*
|
2021-08-08 02:05:45 +01:00
|
|
|
* @throws ServerException when the resource is null
|
|
|
|
*
|
|
|
|
* @return false|int the number of bytes written, false on error
|
2021-07-20 21:17:53 +01:00
|
|
|
*/
|
2021-08-08 02:05:45 +01:00
|
|
|
public function write(string $data): int | false
|
2021-04-25 22:25:56 +01:00
|
|
|
{
|
|
|
|
if (!is_null($this->resource)) {
|
|
|
|
return fwrite($this->resource, $data);
|
|
|
|
} else {
|
2021-08-08 02:05:45 +01:00
|
|
|
// @codeCoverageIgnoreStart
|
|
|
|
throw new TemporaryFileException(_m('Temporary file attempted to write to a null resource'));
|
|
|
|
// @codeCoverageIgnoreEnd
|
2021-04-25 22:25:56 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Closes the file descriptor if opened.
|
|
|
|
*
|
2021-07-20 21:17:53 +01:00
|
|
|
* @return bool true on success or false on failure.
|
2021-04-25 22:25:56 +01:00
|
|
|
*/
|
|
|
|
protected function close(): bool
|
|
|
|
{
|
|
|
|
$ret = true;
|
2021-07-28 22:16:18 +01:00
|
|
|
if (!is_null($this->resource) && $this->resource !== false) {
|
2021-04-25 22:25:56 +01:00
|
|
|
$ret = fclose($this->resource);
|
|
|
|
}
|
|
|
|
if ($ret) {
|
|
|
|
$this->resource = null;
|
|
|
|
}
|
|
|
|
return $ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Closes the file descriptor and removes the temporary file.
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
protected function cleanup(): void
|
|
|
|
{
|
2021-07-28 22:16:18 +01:00
|
|
|
if ($this->resource !== false) {
|
|
|
|
$path = $this->getRealPath();
|
|
|
|
$this->close();
|
|
|
|
if (file_exists($path)) {
|
|
|
|
@unlink($path);
|
|
|
|
}
|
2021-04-25 22:25:56 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the file resource.
|
|
|
|
*
|
|
|
|
* @return resource
|
|
|
|
*/
|
|
|
|
public function getResource()
|
|
|
|
{
|
|
|
|
return $this->resource;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Release the hold on the temporary file and move it to the desired
|
|
|
|
* location, setting file permissions in the process.
|
|
|
|
*
|
2021-07-20 21:17:53 +01:00
|
|
|
* @param string $directory Path where the file should be stored
|
|
|
|
* @param string $filename The filename
|
|
|
|
* @param int $dirmode New directory permissions (in octal mode)
|
|
|
|
* @param int $filemode New file permissions (in octal mode)
|
2021-04-25 22:25:56 +01:00
|
|
|
*
|
|
|
|
* @throws TemporaryFileException
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
2021-07-22 12:32:49 +01:00
|
|
|
public function move(string $directory, string $filename, int $dirmode = 0755, int $filemode = 0644): void
|
2021-04-25 22:25:56 +01:00
|
|
|
{
|
2021-07-20 21:17:53 +01:00
|
|
|
if (!is_dir($directory)) {
|
|
|
|
if (false === @mkdir($directory, $dirmode, true) && !is_dir($directory)) {
|
2021-08-08 02:05:45 +01:00
|
|
|
// @codeCoverageIgnoreStart
|
2021-07-20 21:17:53 +01:00
|
|
|
throw new TemporaryFileException(sprintf('Unable to create the "%s" directory.', $directory));
|
2021-08-08 02:05:45 +01:00
|
|
|
// @codeCoverageIgnoreEnd
|
2021-07-20 21:17:53 +01:00
|
|
|
}
|
|
|
|
} elseif (!is_writable($directory)) {
|
2021-08-08 02:05:45 +01:00
|
|
|
// @codeCoverageIgnoreStart
|
2021-07-20 21:17:53 +01:00
|
|
|
throw new TemporaryFileException(sprintf('Unable to write in the "%s" directory.', $directory));
|
2021-08-08 02:05:45 +01:00
|
|
|
// @codeCoverageIgnoreEnd
|
2021-07-20 21:17:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$destpath = rtrim($directory, '/\\') . DIRECTORY_SEPARATOR . $this->getName($filename);
|
|
|
|
|
2021-07-22 20:56:29 +01:00
|
|
|
$this->commit($destpath, $dirmode, $filemode);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Release the hold on the temporary file and move it to the desired
|
|
|
|
* location, setting file permissions in the process.
|
|
|
|
*
|
|
|
|
* @param string $destpath Full path of destination file
|
|
|
|
* @param int $dirmode New directory permissions (in octal mode)
|
|
|
|
* @param int $filemode New file permissions (in octal mode)
|
|
|
|
*
|
|
|
|
* @throws TemporaryFileException
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function commit(string $destpath, int $dirmode = 0755, int $filemode = 0644): void
|
|
|
|
{
|
2021-04-25 22:25:56 +01:00
|
|
|
$temppath = $this->getRealPath();
|
|
|
|
|
|
|
|
// Might be attempted, and won't end well
|
|
|
|
if ($destpath === $temppath) {
|
|
|
|
throw new TemporaryFileException('Cannot use self as destination');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Memorise if the file was there and see if there is access
|
2021-07-28 22:16:18 +01:00
|
|
|
$existed = file_exists($destpath);
|
2021-07-20 21:17:53 +01:00
|
|
|
|
2021-04-25 22:25:56 +01:00
|
|
|
if (!$this->close()) {
|
2021-05-05 13:44:07 +01:00
|
|
|
// @codeCoverageIgnoreStart
|
2021-04-25 22:25:56 +01:00
|
|
|
throw new TemporaryFileException('Could not close the resource');
|
2021-05-05 13:44:07 +01:00
|
|
|
// @codeCoverageIgnoreEnd
|
2021-04-25 22:25:56 +01:00
|
|
|
}
|
|
|
|
|
2021-07-20 21:17:53 +01:00
|
|
|
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
|
|
|
|
$renamed = rename($this->getPathname(), $destpath);
|
2021-07-28 22:16:18 +01:00
|
|
|
$chmoded = chmod($destpath, $filemode);
|
2021-07-20 21:17:53 +01:00
|
|
|
restore_error_handler();
|
2021-07-28 22:16:18 +01:00
|
|
|
if (!$renamed || !$chmoded) {
|
|
|
|
if (!$existed && file_exists($destpath)) {
|
2021-07-20 21:17:53 +01:00
|
|
|
// If the file wasn't there, clean it up in case of a later failure
|
2021-08-08 02:05:45 +01:00
|
|
|
// @codeCoverageIgnoreStart
|
2021-07-20 21:17:53 +01:00
|
|
|
unlink($destpath);
|
2021-08-08 02:05:45 +01:00
|
|
|
// @codeCoverageIgnoreEnd
|
2021-07-20 21:17:53 +01:00
|
|
|
}
|
|
|
|
throw new TemporaryFileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $destpath, strip_tags($error)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This function is a copy of Symfony\Component\HttpFoundation\File\File->getMimeType()
|
|
|
|
* Returns the mime type of the file.
|
|
|
|
*
|
|
|
|
* The mime type is guessed using a MimeTypeGuesserInterface instance,
|
|
|
|
* which uses finfo_file() then the "file" system binary,
|
|
|
|
* depending on which of those are available.
|
|
|
|
*
|
|
|
|
* @return null|string The guessed mime type (e.g. "application/pdf")
|
|
|
|
*
|
|
|
|
* @see MimeTypes
|
|
|
|
*/
|
|
|
|
public function getMimeType()
|
|
|
|
{
|
2021-08-08 02:05:45 +01:00
|
|
|
// @codeCoverageIgnoreStart
|
2021-07-20 21:17:53 +01:00
|
|
|
if (!class_exists(MimeTypes::class)) {
|
|
|
|
throw new \LogicException('You cannot guess the mime type as the Mime component is not installed. Try running "composer require symfony/mime".');
|
|
|
|
}
|
2021-08-08 02:05:45 +01:00
|
|
|
// @codeCoverageIgnoreEnd
|
2021-07-20 21:17:53 +01:00
|
|
|
|
|
|
|
return MimeTypes::getDefault()->guessMimeType($this->getPathname());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This function is a copy of Symfony\Component\HttpFoundation\File\File->getName()
|
|
|
|
* Returns locale independent base name of the given path.
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
protected function getName(string $name)
|
|
|
|
{
|
|
|
|
$originalName = str_replace('\\', '/', $name);
|
|
|
|
$pos = strrpos($originalName, '/');
|
|
|
|
$originalName = false === $pos ? $originalName : substr($originalName, $pos + 1);
|
|
|
|
|
|
|
|
return $originalName;
|
2021-04-25 22:25:56 +01:00
|
|
|
}
|
|
|
|
}
|