Artifact bcab4fa0505a46f5ed4e4cbc8ab2811b9330b3ab:


<?php

namespace Garradin;

class Fichiers
{
	public $type;
	public $nom;
	public $datetime;
	public $hash;
	public $taille;
	public $id;

	const LIEN_COMPTA = 'compta_journal';
	const LIEN_WIKI = 'wiki_pages';
	const LIEN_MEMBRES = 'membres';

	public function __construct($id)
	{
		$data = DB::getInstance()->simpleQuerySingle('SELECT *, strftime(\'%s\', datetime) AS datetime
			FROM fichiers WHERE id = ?;', true, (int)$id);

		foreach ($data as $key=>$value)
		{
			$this->$key = $value;
		}
	}	

	/**
	 * Envoie une miniature à la taille indiquée au client HTTP
	 * @param  integer $width  Largeur
	 * @param  integer $height Hauteur
	 * @param  boolean $crop   TRUE si on doit cropper aux dimensions indiquées
	 * @return void
	 */
	public function getThumbnail($width, $height, $crop = false)
	{
		// FIXME
	}

	/**
	 * Lier un fichier à un contenu
	 * @param  string $type       Type de contenu (constantes LIEN_*)
	 * @param  integer $foreign_id ID du contenu lié
	 * @return boolean TRUE en cas de succès
	 */
	public function linkTo($type, $foreign_id)
	{
		$db = DB::getInstance();
		$check = [self::LIEN_MEMBRES, self::LIEN_WIKI, self::LIEN_COMPTA];

		if (!in_array($type, $check))
		{
			throw new \LogicException('Type de lien de fichier inconnu.');
		}

		unset($check[array_search($type, $check)]);
		
		foreach ($check as $check_type)
		{
			if ($db->simpleQuerySingle('SELECT 1 FROM fichiers_' . $check_type . ' WHERE fichier = ?;', false, (int)$this->id))
			{
				throw new \LogicException('Ce fichier est déjà lié à un autre contenu : ' . $check_type);
			}
		}

		return $db->simpleExec('INSERT OR IGNORE INTO fichiers_' . $type . ' (fichier, id) VALUES (?, ?);',
			(int)$this->id, (int)$foreign_id);
	}

	/**
	 * Supprime le fichier
	 * @return boolean TRUE en cas de succès
	 */
	public function remove()
	{
		$db = DB::getInstance();
		$db->exec('BEGIN;');
		$db->simpleExec('DELETE FROM fichiers_compta_journal WHERE fichier = ?;', (int)$this->id);
		$db->simpleExec('DELETE FROM fichiers_wiki_pages WHERE fichier = ?;', (int)$this->id);
		$db->simpleExec('DELETE FROM fichiers_membres WHERE fichier = ?;', (int)$this->id);

		// Suppression du contenu s'il n'est pas utilisé par un autre fichier
		if (!($id_contenu = $db->simpleQuerySingle('SELECT id_contenu FROM fichiers AS f1 INNER JOIN fichiers AS f2 
			ON f1.id_contenu = f2.id_contenu AND f1.id != f2.id WHERE f2.id = ?;', false, (int)$this->id)))
		{
			$db->simpleExec('DELETE FROM fichiers_contenu WHERE id = ?;', (int)$id_contenu);
		}

		$db->simpleExec('DELETE FROM fichiers WHERE id = ?;', (int)$this->id);

		return $db->exec('END;');
	}

	/**
	 * Envoie le fichier au client HTTP
	 * @return void
	 */
	public function serve()
	{
		// Le cache est géré par ID contenu, pas ID fichier, pour minimiser l'espace disque utilisé
		$cache_id = 'fichiers.' . $this->id_contenu;

		// Le fichier n'existe pas dans le cache statique, on l'enregistre
		if (!Static_Cache::exists($cache_id))
		{
			$blob = DB::getInstance()->openBlob('fichiers_contenu', 'contenu', (int)$this->id_contenu);
			Static_Cache::storeFromPointer($cache_id, $blob);
			fclose($blob);
		}

		$path = Static_Cache::getPath($cache_id);

		// Désactiver le cache
		header('Pragma: public');
		header('Expires: -1');
		header('Cache-Control: public, must-revalidate, post-check=0, pre-check=0');

		header('Content-Disposition: attachment; filename="' . $this->nom . '"');
		
		// Utilisation de XSendFile si disponible
		if (ENABLE_XSENDFILE && isset($_SERVER['SERVER_SOFTWARE']))
		{
			if (stristr($_SERVER['SERVER_SOFTWARE'], 'apache') 
				&& function_exists('apache_get_modules') 
				&& in_array('mod_xsendfile', apache_get_modules()))
			{
				header('X-Sendfile: ' . $path);
				return true;
			}
			else if (stristr($_SERVER['SERVER_SOFTWARE'], 'lighttpd'))
			{
				header('X-Sendfile: ' . $path);
				return true;
			}
		}

		// Désactiver gzip
		if (function_exists('apache_setenv'))
		{
			@apache_setenv('no-gzip', 1);
		}

		@ini_set('zlib.output_compression', 'Off');

		header('Content-Length: '. (int)$this->taille);

		ob_clean();
		flush();

		// Sinon on envoie le fichier à la mano
		readfile($path);
	}

	/**
	 * Vérifie si le hash fourni n'est pas déjà stocké
	 * Utile pour par exemple reconnaître un ficher dont le contenu est déjà stocké, et éviter un nouvel upload
	 * @param  string $hash Hash SHA1
	 * @return boolean      TRUE si le hash est déjà présent dans fichiers_contenu, FALSE sinon
	 */
	static public function checkHash($hash)
	{
		return (boolean) DB::getInstance()->simpleQuerySingle(
			'SELECT 1 FROM fichiers_contenu WHERE hash = ?;', 
			false, 
			trim(strtolower($hash))
		);
	}

	/**
	 * Retourne un tableau de hash trouvés dans la DB parmi une liste de hash fournis
	 * @param  array  $list Liste de hash à vérifier
	 * @return array        Liste des hash trouvés
	 */
	static public function checkHashList($list)
	{
		$hash_list = '';
		$db = DB::getInstance();

		foreach ($list as $hash)
		{
			$hash_list .= '\'' . $db->escapeString($hash) . '\',';
		}

		$hash_list = substr($hash_list, 0, -1);

		return $db->queryFetchAssoc('SELECT hash, 1
			FROM fichiers_contenu WHERE hash IN (' . $hash_list . ');');
	}

	/**
	 * Récupération du message d'erreur
	 * @param  integer $error Code erreur du $_FILE
	 * @return string Message d'erreur
	 */
	static public function getErrorMessage($error)
	{
		switch ($error)
		{
			case UPLOAD_ERR_INI_SIZE:
				return 'Le fichier excède la taille permise par la configuration du serveur.';
			case UPLOAD_ERR_FORM_SIZE:
				return 'Le fichier excède la taille permise par le formulaire.';
			case UPLOAD_ERR_PARTIAL:
				return 'L\'envoi du fichier a été interrompu.';
			case UPLOAD_ERR_NO_FILE:
				return 'Aucun fichier n\'a été reçu.';
			case UPLOAD_ERR_NO_TMP_DIR:
				return 'Pas de répertoire temporaire pour stocker le fichier.';
			case UPLOAD_ERR_CANT_WRITE:
				return 'Impossible d\'écrire le fichier sur le disque du serveur.';
			case UPLOAD_ERR_EXTENSION:
				return 'Une extension du serveur a interrompu l\'envoi du fichier.';
			default:
				return 'Erreur inconnue: ' . $error;
		}
	}

	/**
	 * Upload du fichier par POST
	 * @param  array  $file  Caractéristiques du fichier envoyé
	 * @return object Un objet Fichiers en cas de succès
	 */
	static public function upload($file)
	{
		if (!empty($file['error']))
		{
			throw new UserException(self::getErrorMessage($file['error']));
		}

		if (empty($file['size']) || empty($file['name']))
		{
			throw new UserException('Fichier reçu invalide : vide ou sans nom de fichier.');
		}

		if (!is_uploaded_file($file['tmp_name']))
		{
			throw new \RuntimeException('Le fichier n\'a pas été envoyé de manière conventionnelle.');
		}

		$name = preg_replace('/[^\d\w._-]/ui', '', $file['name']);

		$bytes = file_get_contents($file['tmp_name'], false, null, -1, 1024);
		$type = \KD2\FileInfo::guessMimeType($bytes);

		if (!$type)
		{
			$ext = substr($name, strrpos($name, '.')+1);
			$ext = strtolower($ext);

			$type = \KD2\FileInfo::getMimeTypeFromFileExtension($ext);
		}

		$is_image = preg_match('/^image\//', $type);

		$hash = sha1_file($file['tmp_name']);
		$size = filesize($file['tmp_name']);

		$db = DB::getInstance();
		$db->exec('BEGIN;');

		$db->simpleInsert('fichiers_contenu', [
			'hash'		=>	$hash,
			'taille'	=>	(int)$size,
			'contenu'	=>	[\SQLITE3_BLOB, file_get_contents($file['tmp_name'])],
		]);

		$id_contenu = $db->lastInsertRowID();

		$db->simpleInsert('fichiers', [
			'id_contenu'	=>	(int)$id_contenu,
			'nom'			=>	$name,
			'type'			=>	$type,
			'image'			=>	(int)$is_image,
		]);

		$db->exec('END;');

		return new Fichiers($db->lastInsertRowID());
	}

	/**
	 * Envoie un fichier déjà stocké
	 * @param  string $name Nom du fichier
	 * @param  string $hash Hash SHA1 du contenu du fichier
	 * @return object       Un objet Fichiers en cas de succès
	 */
	static public function uploadExistingHash($name, $hash)
	{
		$db = DB::getInstance();
		$name = preg_replace('/[^\d\w._-]/ui', '', $name);

		$file = $db->simpleQuerySingle('SELECT * FROM fichiers 
			INNER JOIN fichiers_contenu AS fc ON fc.id = fichiers.id_contenu AND fc.hash = ?;', true, trim($hash));

		if (!$file)
		{
			throw new UserException('Le fichier à copier n\'existe pas (aucun hash ne correspond à '.$hash.').');
		}

		$db->simpleInsert('fichiers', [
			'id_contenu'	=>	(int)$file['id_contenu'],
			'nom'			=>	$name,
			'type'			=>	$file['type'],
			'image'			=>	(int)$file['image'],
		]);

		return new Fichiers($db->lastInsertRowID());
	}

	static public function SkrivHTML($args, $content, $skriv)
	{
		if (empty($args['id']) && !empty($content))
		{
			$args = ['id' => (int)$content];
		}

		if (empty($args['id']))
		{
			return $this->parseError('Aucun numéro de fichier indiqué.');
		}

		$db = DB::getInstance();

		// FIXME
	}
}