diff --git a/src/Util/TemporaryFile.php b/src/Util/TemporaryFile.php new file mode 100644 index 0000000000..336bdea2e5 --- /dev/null +++ b/src/Util/TemporaryFile.php @@ -0,0 +1,151 @@ +. +// }}} + +namespace App\Util; + +/** + * Class oriented at providing automatic temporary file handling. + * + * @package GNUsocial + * + * @author Alexei Sorokin + * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class TemporaryFile extends \SplFileInfo +{ + protected $resource; + + /** + * @param null|string $prefix The file name will begin with that prefix + * ("php" by default) + * @param null|string $mode File open mode ("w+b" by default) + */ + public function __construct( + ?string $prefix = null, + ?string $mode = null + ) { + $filename = tempnam(sys_get_temp_dir(), $prefix ?? 'gs-php'); + + if ($filename === false) { + throw new TemporaryFileException('Could not create file: ' . $filename); + } + + parent::__construct($filename); + + if (($this->resource = fopen($filename, $mode ?? 'w+b')) === false) { + $this->cleanup(); + throw new TemporaryFileException('Could not open file: ' . $filename); + } + } + + public function __destruct() + { + $this->close(); + $this->cleanup(); + } + + public function write($data): int + { + if (!is_null($this->resource)) { + return fwrite($this->resource, $data); + } else { + return null; + } + } + + /** + * Closes the file descriptor if opened. + * + * @return bool Whether successful + */ + protected function close(): bool + { + $ret = true; + if (!is_null($this->resource)) { + $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 + { + $path = $this->getRealPath(); + $this->close(); + if (file_exists($path)) { + unlink($path); + } + } + + /** + * 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. + * + * @param string File destination + * @param int New file permissions (in octal mode) + * + * @throws TemporaryFileException + * + * @return void + */ + public function commit(string $destpath, int $umode = 0644): void + { + $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 + $exists = file_exists($destpath); + if (!touch($destpath)) { + throw new TemporaryFileException( + 'Insufficient permissions for destination: "' . $destpath . '"' + ); + } elseif (!$exists) { + // If the file wasn't there, clean it up in case of a later failure + unlink($destpath); + } + if (!$this->close()) { + throw new TemporaryFileException('Could not close the resource'); + } + + rename($temppath, $destpath); + chmod($destpath, $umode); + } +}