<?php

/**
 * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
 * SPDX-License-Identifier: AGPL-3.0-only
 */
namespace OC;

use bantu\IniGetWrapper\IniGetWrapper;
use OCP\IConfig;
use OCP\ITempManager;
use Psr\Log\LoggerInterface;

class TempManager implements ITempManager {
	/** @var string[] Current temporary files and folders, used for cleanup */
	protected $current = [];
	/** @var string i.e. /tmp on linux systems */
	protected $tmpBaseDir;
	/** @var LoggerInterface */
	protected $log;
	/** @var IConfig */
	protected $config;
	/** @var IniGetWrapper */
	protected $iniGetWrapper;

	/** Prefix */
	public const TMP_PREFIX = 'oc_tmp_';

	public function __construct(LoggerInterface $logger, IConfig $config, IniGetWrapper $iniGetWrapper) {
		$this->log = $logger;
		$this->config = $config;
		$this->iniGetWrapper = $iniGetWrapper;
		$this->tmpBaseDir = $this->getTempBaseDir();
	}

	/**
	 * Builds the filename with suffix and removes potential dangerous characters
	 * such as directory separators.
	 *
	 * @param string $absolutePath Absolute path to the file / folder
	 * @param string $postFix Postfix appended to the temporary file name, may be user controlled
	 * @return string
	 */
	private function buildFileNameWithSuffix($absolutePath, $postFix = '') {
		if ($postFix !== '') {
			$postFix = '.' . ltrim($postFix, '.');
			$postFix = str_replace(['\\', '/'], '', $postFix);
			$absolutePath .= '-';
		}

		return $absolutePath . $postFix;
	}

	/**
	 * Create a temporary file and return the path
	 *
	 * @param string $postFix Postfix appended to the temporary file name
	 * @return string
	 */
	public function getTemporaryFile($postFix = '') {
		if (is_writable($this->tmpBaseDir)) {
			// To create an unique file and prevent the risk of race conditions
			// or duplicated temporary files by other means such as collisions
			// we need to create the file using `tempnam` and append a possible
			// postfix to it later
			$file = tempnam($this->tmpBaseDir, self::TMP_PREFIX);
			$this->current[] = $file;

			// If a postfix got specified sanitize it and create a postfixed
			// temporary file
			if ($postFix !== '') {
				$fileNameWithPostfix = $this->buildFileNameWithSuffix($file, $postFix);
				touch($fileNameWithPostfix);
				chmod($fileNameWithPostfix, 0600);
				$this->current[] = $fileNameWithPostfix;
				return $fileNameWithPostfix;
			}

			return $file;
		} else {
			$this->log->warning(
				'Can not create a temporary file in directory {dir}. Check it exists and has correct permissions',
				[
					'dir' => $this->tmpBaseDir,
				]
			);
			return false;
		}
	}

	/**
	 * Create a temporary folder and return the path
	 *
	 * @param string $postFix Postfix appended to the temporary folder name
	 * @return string
	 */
	public function getTemporaryFolder($postFix = '') {
		if (is_writable($this->tmpBaseDir)) {
			// To create an unique directory and prevent the risk of race conditions
			// or duplicated temporary files by other means such as collisions
			// we need to create the file using `tempnam` and append a possible
			// postfix to it later
			$uniqueFileName = tempnam($this->tmpBaseDir, self::TMP_PREFIX);
			$this->current[] = $uniqueFileName;

			// Build a name without postfix
			$path = $this->buildFileNameWithSuffix($uniqueFileName . '-folder', $postFix);
			mkdir($path, 0700);
			$this->current[] = $path;

			return $path . '/';
		} else {
			$this->log->warning(
				'Can not create a temporary folder in directory {dir}. Check it exists and has correct permissions',
				[
					'dir' => $this->tmpBaseDir,
				]
			);
			return false;
		}
	}

	/**
	 * Remove the temporary files and folders generated during this request
	 */
	public function clean() {
		$this->cleanFiles($this->current);
	}

	/**
	 * @param string[] $files
	 */
	protected function cleanFiles($files) {
		foreach ($files as $file) {
			if (file_exists($file)) {
				try {
					\OC_Helper::rmdirr($file);
				} catch (\UnexpectedValueException $ex) {
					$this->log->warning(
						'Error deleting temporary file/folder: {file} - Reason: {error}',
						[
							'file' => $file,
							'error' => $ex->getMessage(),
						]
					);
				}
			}
		}
	}

	/**
	 * Remove old temporary files and folders that were failed to be cleaned
	 */
	public function cleanOld() {
		$this->cleanFiles($this->getOldFiles());
	}

	/**
	 * Get all temporary files and folders generated by oc older than an hour
	 *
	 * @return string[]
	 */
	protected function getOldFiles() {
		$cutOfTime = time() - 3600;
		$files = [];
		$dh = opendir($this->tmpBaseDir);
		if ($dh) {
			while (($file = readdir($dh)) !== false) {
				if (substr($file, 0, 7) === self::TMP_PREFIX) {
					$path = $this->tmpBaseDir . '/' . $file;
					$mtime = filemtime($path);
					if ($mtime < $cutOfTime) {
						$files[] = $path;
					}
				}
			}
		}
		return $files;
	}

	/**
	 * Get the temporary base directory configured on the server
	 *
	 * @return string Path to the temporary directory or null
	 * @throws \UnexpectedValueException
	 */
	public function getTempBaseDir() {
		if ($this->tmpBaseDir) {
			return $this->tmpBaseDir;
		}

		$directories = [];
		if ($temp = $this->config->getSystemValue('tempdirectory', null)) {
			$directories[] = $temp;
		}
		if ($temp = $this->iniGetWrapper->get('upload_tmp_dir')) {
			$directories[] = $temp;
		}
		if ($temp = getenv('TMP')) {
			$directories[] = $temp;
		}
		if ($temp = getenv('TEMP')) {
			$directories[] = $temp;
		}
		if ($temp = getenv('TMPDIR')) {
			$directories[] = $temp;
		}
		if ($temp = sys_get_temp_dir()) {
			$directories[] = $temp;
		}

		foreach ($directories as $dir) {
			if ($this->checkTemporaryDirectory($dir)) {
				return $dir;
			}
		}

		$temp = tempnam(__DIR__, '');
		if (file_exists($temp)) {
			unlink($temp);
			return dirname($temp);
		}
		throw new \UnexpectedValueException('Unable to detect system temporary directory');
	}

	/**
	 * Check if a temporary directory is ready for use
	 *
	 * @param mixed $directory
	 * @return bool
	 */
	private function checkTemporaryDirectory($directory) {
		// suppress any possible errors caused by is_writable
		// checks missing or invalid path or characters, wrong permissions etc
		try {
			if (is_writable($directory)) {
				return true;
			}
		} catch (\Exception $e) {
		}
		$this->log->warning('Temporary directory {dir} is not present or writable',
			['dir' => $directory]
		);
		return false;
	}

	/**
	 * Override the temporary base directory
	 *
	 * @param string $directory
	 */
	public function overrideTempBaseDir($directory) {
		$this->tmpBaseDir = $directory;
	}
}