<?php
// 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/>.

/**
 * @package   GNUsocial
 * @author    Alexei Sorokin <sor.alexei@meowr.ru>
 * @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 <sor.alexei@meowr.ru>
 * @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 <sor.alexei@meowr.ru>
 * @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);
    }
}
