. /** * @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 */ defined('GNUSOCIAL') || die(); /** * Exception wrapper for TemporaryFile errors * * @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 TemporaryFileException extends Exception { } /** * 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 = null; /** * @param string|null $prefix The file name will begin with that prefix * ("php" by default) * @param string|null $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(); } /** * 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) * @return void * @throws TemporaryFileException */ 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); } }