<?php
/*
 * Copyright 2009-2013 Florian "Bluewind" Pritz <bluewind@server-speed.net>
 *
 * Licensed under AGPLv3
 * (see COPYING for full license text)
 *
 */

class Mfile extends CI_Model {

	private $upload_path;
	private $id_validation_config;

	function __construct()
	{
		$this->load->model("muser");

		$this->upload_path = $this->config->item('upload_path');
		$this->id_validation_config = array(
			"upload_max_age" => $this->config->item("upload_max_age"),
			"small_upload_size" => $this->config->item("small_upload_size"),
			"sess_expiration" => $this->config->item("sess_expiration"),
		);
	}

	// Returns an unused ID
	function new_id($min = 3, $max = 6)
	{
		static $id_blacklist = NULL;

		if ($id_blacklist == NULL) {
			// This prevents people from being unable to access their uploads
			// because of URL rewriting
			$id_blacklist = scandir(FCPATH);
			$id_blacklist[] = "file";
			$id_blacklist[] = "user";
		}

		$max_tries = 100;

		for ($try = 0; $try < $max_tries; $try++) {
			$id = random_alphanum($min, $max);

			if ($this->id_exists($id) || in_array($id, $id_blacklist)) {
				continue;
			}

			return $id;
		}

		throw new \exceptions\PublicApiException("file/new_id-try-limit", "Failed to find unused ID after $max_tries tries");
	}

	function id_exists($id)
	{
		if (!$id) {
			return false;
		}

		$query = $this->db->select('id')
			->from('files')
			->where('id', $id)
			->limit(1)
			->get();

		return $query->num_rows() == 1;
	}

	function get_filedata($id)
	{
		return cache_function("filedatav2-$id", 300, function() use ($id) {
			$query = $this->db
				->select('files.id, hash, file_storage.id storage_id, filename, mimetype, files.date, user, filesize')
				->from('files')
				->join('file_storage', 'file_storage.id = files.file_storage_id')
				->where('files.id', $id)
				->limit(1)
				->get();

			if ($query->num_rows() > 0) {
				$data = $query->row_array();
				$data["data_id"] = $data["hash"]."-".$data["storage_id"];
				return $data;
			} else {
				return false;
			}
		});
	}

	// return the folder in which the file with $data_id is stored
	function folder($data_id) {
		return $this->upload_path.'/'.substr($data_id, 0, 3);
	}

	// Returns the full path to the file with $data_id
	function file($data_id) {
		return $this->folder($data_id).'/'.$data_id;
	}

	// Add a file to the DB
	function add_file($userid, $id, $filename, $storage_id)
	{
		$this->db->insert("files", array(
			"id" => $id,
			"filename" => $filename,
			"date" => time(),
			"user" => $userid,
			"file_storage_id" => $storage_id,
		));
	}

	function adopt($id)
	{
		$userid = $this->muser->get_userid();

		$this->db->set(array('user' => $userid ))
			->where('id', $id)
			->where('user', 0)
			->update('files');
		return $this->db->affected_rows();
	}

	// remove old/invalid/broken IDs
	public function valid_id($id)
	{
		$filedata = $this->get_filedata($id);

		if (!$filedata) {
			return false;
		}

		return $this->valid_filedata($filedata);
	}

	public function valid_filedata($filedata)
	{
		return \service\files::valid_id($filedata, $this->id_validation_config, $this, time());
	}

	public function file_exists($file)
	{
		return file_exists($file);
	}

	public function filemtime($file)
	{
		return filemtime($file);
	}

	public function filesize($file)
	{
		return filesize($file);
	}

	public function get_timeout($id)
	{
		$filedata = $this->get_filedata($id);
		$file = $this->file($filedata["data_id"]);

		if ($this->config->item("upload_max_age") == 0) {
			return -1;
		}

		if (filesize($file) > $this->config->item("small_upload_size")) {
			return $filedata["date"] + $this->config->item("upload_max_age");
		} else {
			return -1;
		}
	}

	public function get_timeout_string($id)
	{
		$timeout = $this->get_timeout($id);

		if ($timeout >= 0) {
			return date("r", $timeout);
		} else {
			return "unknown";
		}
	}

	private function unused_file($data_id)
	{
		list ($hash, $storage_id) = explode("-", $data_id);
		$query = $this->db->select('files.id')
			->from('files')
			->join('file_storage', 'file_storage.id = files.file_storage_id')
			->where('hash', $hash)
			->where('file_storage.id', $storage_id)
			->limit(1)
			->get();

		return $query->num_rows() == 0;
	}

	public function delete_by_user($userid)
	{
		$query = $this->db->select("id")
			->where("user", $userid)
			->get("files")->result_array();
		$ids = array_map(function ($a) {return $a['id'];}, $query);

		foreach ($ids as $id) {
			$this->delete_id($id);
		}
	}

	public function delete_id($id)
	{
		$filedata = $this->get_filedata($id);

		// Delete the file and all multipastes using it
		// Note that this does not delete all relations in multipaste_file_map
		// which is actually done by an SQL contraint.
		// TODO: make it work properly without the constraint
		$map = $this->db->select('url_id')
			->distinct()
			->from('multipaste_file_map')
			->join("multipaste", "multipaste.multipaste_id = multipaste_file_map.multipaste_id")
			->where('file_url_id', $id)
			->get()->result_array();

		$this->db->where('id', $id)
			->delete('files');
		delete_cache("filedata-$id");

		foreach ($map as $entry) {
			assert(!empty($entry['url_id']));
			$this->mmultipaste->delete_id($entry["url_id"]);
		}

		if ($this->id_exists($id))  {
			return false;
		}

		if ($filedata !== false) {
			assert(isset($filedata["data_id"]));
			if ($this->unused_file($filedata['data_id'])) {
				$this->delete_data_id($filedata['data_id']);
			}
		}
		return true;
	}

	public function delete_data_id($data_id)
	{
		list ($hash, $storage_id) = explode("-", $data_id);

		$this->db->where('id', $storage_id)
			->delete('file_storage');
		if (file_exists($this->file($data_id))) {
			unlink($this->file($data_id));
		}
		$dir = $this->folder($data_id);
		if (file_exists($dir)) {
			if (count(scandir($dir)) == 2) {
				rmdir($dir);
			}
		}
		delete_cache("{$data_id}_thumb_150");
	}

	public function get_owner($id)
	{
		return $this->db->select('user')
			->from('files')
			->where('id', $id)
			->get()->row_array()
			['user'];
	}

}

# vim: set noet:
