Valider b014336c rédigé par marcimat's avatar marcimat
Parcourir les fichiers

Encore de gros changements.

On gère les hits de téléversement de morceaux de fichiers
ou de tests de présence de morceaux de fichiers déjà uploadés
dans le fichier d'option directement.

On renomme le nom du token envoyé en 'bigup_token' pour mieux le discriminer
d'autres variables. Dès que cette clé est présente, on charge la mécanique
de test ou d'enregistrement des fichiers, et on quitte directement,
en retournant le bon statut http, tel qu'attendu par la librairie js.

L'inconvénient c'est qu'on ne peut pas directement transmettre des messages
d'erreurs spécialisés pour tel ou tel formulaire, mais ce sera le formulaire
qui testera ensuite s'il considère valide le fichier qu'il a reçu.
Ceci dit, on pourrait au moins indiquer une erreur si on tente d'envoyer un fichier
trop gros par rapport à la configuration choisie.

Autre point, on sépare en 3 la classe Flow.
- 1 trait pour gérer les logs
- 1 classe qui gère simplement la validité du token et la réception des paramètres SPIP (bigup)
- flow, qui gère la réception de ses paramètres, la réception ou test des morceaux et la recombinaison du fichier.

Y aurait mieux à faire, mais c'est déjà pas mal.
parent 6aac3f8d
Chargement en cours
Chargement en cours
Chargement en cours
Chargement en cours
+0 −1
Numéro de ligne d'origine Numéro de ligne de diff Ligne de diff
@@ -119,7 +119,6 @@ function bigup_sous_repertoires($dest){
			return false;
		}
		$base = sous_repertoire($base, array_pop($create));
		spip_log("+ " . $base);
		if (!$base) {
			return false;
		}
+16 −2
Numéro de ligne d'origine Numéro de ligne de diff Ligne de diff
@@ -10,3 +10,17 @@
 */

if (!defined('_ECRIRE_INC_VERSION')) return;


/**
 * Assez tôt on vérifie si on demande à tester la présence d'un morceau de fichier uploadé
 * ou si on demande à envoyer un morceau de fichier.
 *
 * Flow vérifie évidement que l'accès est accrédité !
**/
if (_request('bigup_token')) {
	include_spip('inc/Bigup');
	$Bigup = new \SPIP\Bigup\Bigup();
	$Bigup->repondre();
	exit;
}
+10 −6
Numéro de ligne d'origine Numéro de ligne de diff Ligne de diff
@@ -59,18 +59,21 @@ function bigup_header_prive($flux) {
 * @return array
**/
function bigup_formulaire_charger($flux) {

/*
	// S'il y a des champs fichiers de déclarés
	if ($fichiers = bigup_lister_fichiers_formulaire($flux['args']['form'], $flux['args']['args'])) {
		$flux['data']['_fichiers'] = $fichiers;
	}

	}*/
/*
	$form = $flux['args']['form'];
	$args = $flux['args']['args'];
	array_unshift($args, $GLOBALS['spip_lang']);
	$formulaire_args = encoder_contexte_ajax($args, $form);
*/
	// Si le formulaire est considéré posté (en get ou post)
	// Tester voir si c'est pas un morceaux de fichier qui est envoyé
	if (!empty($flux['args']['je_suis_poste'])) {

		spip_log('je_suis_poste', 'test_upl');

		/*
		include_spip('inc/flow');
		$Flow = new \SPIP\Bigup\Flow();
		$key = $Flow->run();
@@ -78,6 +81,7 @@ function bigup_formulaire_charger($flux) {
			spip_log("Fichier reçu dans $key", 'test_upl');
			spip_log($_FILES[$key], 'test_upl');
		}
		*/
	}

	return $flux;

inc/Bigup.php

0 → 100644
+276 −0
Numéro de ligne d'origine Numéro de ligne de diff Ligne de diff
<?php

namespace Spip\Bigup;

/**
 * Mappage entre Bigup et Flow
 *
 * @plugin     Bigup
 * @copyright  2015
 * @author     marcimat
 * @licence    GNU/GPL
 * @package    SPIP\Bigup\Fonctions
 */

include_spip('inc/Bigup/LogTrait');
include_spip('inc/Bigup/Flow');

/**
 * Gère la validité des requêtes et appelle Flow
**/
class Bigup {

	use LogTrait;

	/**
	 * Login ou identifiant de l'auteur qui intéragit
	 * @var string */
	private $auteur = '';

	/**
	 * Nom du formulaire qui utilise flow
	 * @var string */
	private $formulaire = '';

	/**
	 * Hash des arguments du formulaire
	 * @var string */
	private $formulaire_args = '';

	/**
	 * Identifie un formulaire par rapport à un autre identique sur la même page ayant un appel différent.
	 * @var string */
	private $formulaire_identifiant = '';

	/**
	 * Nom du champ dans le formulaire qui utilise flow
	 * @var string */
	private $champ = '';

	/**
	 * Token de la forme `champ:time:cle`
	 * @var string
	**/
	private $token = '';
	
	/**
	 * Expiration du token (en secondes)
	 *
	 * @todo À définir en configuration
	 * @var int
	**/
	private $token_expiration = 3600 * 24;

	/**
	 * Nom du répertoire, dans _DIR_TMP, qui va stocker les fichiers et morceaux de fichiers
	 * @var string */
	private $cache_dir = 'bigupload';

	/**
	 * Chemin du répertoire stockant les morceaux de fichiers
	 * @var string */
	 private $dir_parts = '';

	/**
	 * Chemin du répertoire stockant les fichiers terminés
	 * @var string */
	 private $dir_final = '';


	/**
	 * Constructeur
	**/
	public function __construct() {
		$this->trouver_parametres();
		$this->identifier_auteur();
	}

	/**
	 * Retrouve les paramètres pertinents pour gérer le test ou la réception de fichiers.
	**/
	public function trouver_parametres() {
		$this->token           = _request('bigup_token');
		$this->formulaire      = _request('formulaire_action');
		$this->formulaire_args = _request('formulaire_action_args');
	}

	/**
	 * Répondre
	 *
	 * Envoie un statut HTTP de réponse et quitte, en fonction de ce qui était demandé,
	 * soit tester un morceau de fichier, soit réceptionner un morceau de fichier.
	 *
	 * Si les hash ne correspondaient pas, le programme quitte évidemment.
	**/
	public function repondre() {
		if (!$this->verifier_token()) {
			return $this->send(415);
		}

		$this->calculer_chemin_repertoires();

		include_spip('inc/Bigup/Flow');
		$flow = new Flow();
		$flow->definir_repertoire('parts', $this->dir_parts);
		$flow->definir_repertoire('final', $this->dir_final);
		$res = $flow->run();

		if (is_string($res)) {
			// remettre le fichier dans $FILES
			# $this->integrer_fichier($this->champ, $res);

			// on demande à nettoyer le répertoire des fichiers dans la foulée
			job_queue_add(
				'bigup_nettoyer_repertoire_upload',
				'Nettoyer répertoires et fichiers de Big Upload',
				array(0),
				'genie/'
			);
			$this->send(200);
		}

		if (is_int($res)) {
			$this->send($res);
		}

		$this->send(415);
	}


	/**
	 * Vérifier le token utilisé
	 *
	 * Le token doit arriver, de la forme `champ:time:clé`
	 * De même que formulaire_action et formulaire_action_args
	 *
	 * Le temps ne doit pas être trop vieux d'une part,
	 * et la clé de sécurité doit évidemment être valide.
	 * 
	 * @return bool
	**/
	public function verifier_token() {
		if (!$this->token) {
			$this->debug("Aucun token");
			return false;
		}

		$_token = explode(':', $this->token);

		if (count($_token) != 3) {
			$this->debug("Token mal formé");
			return false;
		}

		list($champ, $time, $cle) = $_token;
		$time = intval($time);
		$now = time();


		if (($now - $time) > $this->token_expiration) {
			$this->log("Token expiré");
			return false;
		}

		if (!$this->formulaire) {
			$this->log("Vérifier token : nom du formulaire absent");
			return false;
		}

		if (!$this->formulaire_args) {
			$this->log("Vérifier token : hash du formulaire absent");
			return false;
		}

		include_spip('inc/securiser_action');
		if (!verifier_action_auteur("bigup/$this->formulaire/$this->formulaire_args/$champ/$time", $cle)) {
			$this->error("Token invalide");
			return false;
		}

		// Renseigner le formulaire et champ utilisé.
		$identifiant = substr($this->formulaire_args, 0, 6);
		$this->formulaire_identifiant = $identifiant;
		$this->champ = $champ;

		$this->debug("Token OK : formulaire $this->formulaire, champ $champ, identifiant $identifiant");

		return true;
	}


	/**
	 * Calcule les chemins des répertoires de travail
	 * qui stockent les morceaux de fichiers et les fichiers complétés
	**/
	public function calculer_chemin_repertoires() {
		$this->dir_parts = $this->calculer_chemin_repertoire('parts');
		$this->dir_final = $this->calculer_chemin_repertoire('final');
	}

	/**
	 * Calcule un chemin de répertoire de travail d'un type donné
	 * @return string
	**/
	public function calculer_chemin_repertoire($type) {
		return
			_DIR_TMP . $this->cache_dir
			. DIRECTORY_SEPARATOR . $type
			. DIRECTORY_SEPARATOR . $this->auteur
			. DIRECTORY_SEPARATOR . $this->formulaire
			. DIRECTORY_SEPARATOR . $this->formulaire_identifiant
			. DIRECTORY_SEPARATOR . $this->champ;
	}

	/**
	 * Identifier l'auteur qui accède
	 *
	 * @todo
	 *     Gérer le cas des auteurs anonymes, peut être avec l'identifiant de session php.
	 *
	 * @return string
	**/
	public function identifier_auteur() {
		// un nom d'identifiant humain si possible
		include_spip('inc/session');
		if (!$identifiant = session_get('login')) {
			$identifiant = session_get('id_auteur');
		}
		return $this->auteur = $identifiant;
	}

	/**
	 * Intégrer le fichier indiqué dans `$FILES`
	 *
	 * @param string $key Clé d'enregistrement
	 * @param string $chemin
	 * @return string Clé d'enregistrement
	**/
	public function integrer_fichier($key, $chemin) {
		$filename = basename($chemin);

		// on réécrit $_FILES avec les valeurs du fichier complet
		$_FILES[$key]['name'] = $filename;
		$_FILES[$key]['tmp_name'] = $chemin;
		$_FILES[$key]['size'] = filesize($chemin);
		$finfo = finfo_open(FILEINFO_MIME_TYPE);
		$_FILES[$key]['type'] = finfo_file($finfo, $chemin);

		// fichier complété
		return $key;
	}


	/**
	 * Envoie le code header indiqué… et arrête tout.
	 *
	 * @param int $code
	 * @return void
	**/
	public function send($code) {
		$this->debug("> send $code");
		http_response_code($code);
		exit;
	}


}
+65 −150
Numéro de ligne d'origine Numéro de ligne de diff Ligne de diff
@@ -12,18 +12,22 @@ namespace Spip\Bigup;
 * @link https://github.com/dilab/resumable.php Inspiration
 * @link https://github.com/flowjs/flow-php-server Autre implémentation pour Flow.
 *
 * @plugin     Dropzone
 * @plugin     Bigup
 * @copyright  2015
 * @author     marcimat
 * @licence    GNU/GPL
 * @package    SPIP\Bigup\Fonctions
 */
 
include_spip('inc/Bigup/LogTrait');

/**
 * Réceptionne des morceaux de fichiers envoyés par flow.js
**/
class Flow {

	use LogTrait;

	/**
	 * Chemins des répertoires de travail (dans _DIR_TMP)
	 * Peut contenir la clé :
@@ -68,6 +72,17 @@ class Flow {
	**/
	public function __construct() {}


	/**
	 * Définir les répertoires de travail
	 *
	 * @param string $type
	 * @param string $dir
	**/
	public function definir_repertoire($type, $chemin) {
		$this->dir[$type] = $chemin;
	}

	/**
	 * Trouve le prefixe utilisé pour envoyer les données
	 *
@@ -91,27 +106,6 @@ class Flow {
	}


	/**
	 * Des logs
	 *
	 * @param mixed $quoi
	 * @param gravite $quoi
	**/
	public function log($quoi, $gravite = _LOG_INFO_IMPORTANTE) {
		spip_log($quoi, "bigup." . $gravite);
	}

	public function debug($quoi) {
		return $this->log($quoi, _LOG_DEBUG);
	}

	public function error($quoi) {
		return $this->log($quoi, _LOG_ERREUR);
	}

	public function info($quoi) {
		return $this->log($quoi, _LOG_INFO);
	}

	/**
	 * Tester l'arrivée du javascript et agir en conséquence
@@ -130,9 +124,6 @@ class Flow {
		if (!$this->trouverPrefixe()) {
			return false;
		}
		if (!$this->verifier_token()) {
			return $this->send(415);
		}
		if (!empty($_POST) and !empty($_FILES) ) {
			return $this->handleChunk();
		}
@@ -142,64 +133,6 @@ class Flow {
		return false;
	}

	/**
	 * Vérifier le token utilisé
	 *
	 * Le token doit arriver, de la forme `champ:time:clé`
	 * De même que formulaire_action et formulaire_action_args
	 * 
	 * @return bool
	**/
	public function verifier_token() {
		if (!$token = _request('token')) {
			$this->debug("Aucun token");
			return false;
		}

		$_token = explode(':', $token);

		if (count($_token) != 3) {
			$this->debug("Token mal formé");
			return false;
		}
		list($champ, $time, $cle) = $_token;
		$time = intval($time);
		$now = time();

		// 24h de validité
		// TODO: à définir en configuration
		if (($now - $time) > (60 * 60 * 24)) {
			$this->log("Token expiré");
			return false;
		}

		$form = _request('formulaire_action');
		if (!$form) {
			$this->log("Vérifier token : nom du formulaire absent");
			return false;
		}

		$form_args = _request('formulaire_action_args');
		if (!$form_args) {
			$this->log("Vérifier token : hash du formulaire absent");
			return false;
		}

		if (!verifier_action_auteur("bigup/$form/$form_args/$champ/$time", $cle)) {
			$this->error("Token invalide");
			return false;
		}

		// Renseigner le formulaire et champ utilisé.
		$identifiant = substr($form_args, 0, 6);
		$this->debug("Token OK : formulaire $form, champ $champ, identifiant $identifiant");

		$this->formulaire = $form;
		$this->formulaire_identifiant = $identifiant;
		$this->champ = $champ;

		return true;
	}

	/**
	 * Envoie le code header indiqué… et arrête tout.
@@ -237,7 +170,7 @@ class Flow {
	 *
	 * @return void|false|string
	 *     - exit : Si morceau de fichier reçu (et que ce n'est pas le dernier), la fonction retourne un statut http et quitte.
	 *     - string : Si fichier terminé d'uploader (réception du dernier morceau), retourne la clé utilisée dans `$_FILES` pour le décrire
	 *     - string : Si fichier terminé d'uploader (réception du dernier morceau), retourne le chemin du fichier
	 *     - false  : si aucun morceau de fichier reçu.
	**/
	public function handleChunk() {
@@ -268,43 +201,14 @@ class Flow {
			// liste des morceaux
			$chunkFiles = $this->getChunkFiles($identifier);

			if ($fullFile = $this->createFileFromChunks($chunkFiles, $this->tmpPathFile($identifier, $filename))) {
				$this->info("Fichier complet recréé : " . $this->tmpPathFile($identifier, $filename));
				$this->info("Suppression des morceaux.");
				foreach ($chunkFiles as $f) {
					@unlink($f);
				}

				// on demande à nettoyer le répertoire des fichiers dans la foulée
				job_queue_add(
					'bigup_nettoyer_repertoire_upload',
					'Nettoyer répertoires et fichiers de Big Upload',
					array(0),
					'genie/'
				);

				// on réécrit $_FILES avec les valeurs du fichier complet
				$_FILES[$key]['name'] = $filename;
				$_FILES[$key]['tmp_name'] = $fullFile;
				$_FILES[$key]['size'] = filesize($fullFile);
				$finfo = finfo_open(FILEINFO_MIME_TYPE);
				$_FILES[$key]['type'] = finfo_file($finfo, $fullFile);
				$this->debug($_FILES);

				// On n'envoie rien (pas de $this->send()) : ici le fichier étant bien arrivé
				// on laisse le processus suivant se faire,
				// comme si le fichier complet avait été posté dans $_FILES
				// sur ce hit.
				

				// fichier complété
				return $key;

			} else {
			$fullFile = $this->createFileFromChunks($chunkFiles, $this->tmpPathFile($identifier, $filename));
			if (!$fullFile) {
				// on ne devrait jamais arriver là ! 
				$this->error("! Création du fichier complet en échec (" . $this->tmpPathFile($identifier, $filename) . ").");
				return $this->send(415);
			}

			return $fullFile;
		} else {
			// morceau bien reçu, mais pas encore le dernier… 
			return $this->send(200);
@@ -327,36 +231,30 @@ class Flow {
	/**
	 * Trouver le chemin d'un répertoire temporaire 
	 *
	 * Dépend de l'auteur connecté.
	 *
	 * @param string $identifier
	 * @param string $subdir Type de répertoire
	 * @return string chemin du répertoire
	 * @param bool $nocreate true pour ne pas créer le répertoire s'il manque.
	 * @return string|false
	 *     - string : chemin du répertoire
	 *     - false : échec.
	**/
	public function determine_upload($identifier = null, $subdir) {
		if (!function_exists('bigup_sous_repertoires')) {
			include_spip('bigup_fonctions');
		}
	public function determine_upload($identifier, $subdir, $nocreate = false) {
		if (empty($this->dir[$subdir])) {
			include_spip('inc/session');
			// un nom de répertoire humain si possible
			if (!$login = session_get('login')) {
				$login = session_get('id_auteur');
			}
			$chemin = [
				_DIR_TMP . $this->cache_dir,
				$subdir, $login,
				$this->formulaire, $this->formulaire_identifiant, $this->champ
			];
			$chemin = implode('/', $chemin);
			$this->dir[$subdir] = bigup_sous_repertoires($chemin);
		}

		if ($identifier) {
			return sous_repertoire($this->dir[$subdir], $identifier);
		} else {
			return $this->dir[$subdir];
			return false;
		}

		$dir = $this->dir[$subdir] . DIRECTORY_SEPARATOR . $identifier;

		if ($nocreate) {
			return $dir;
		}

		include_spip('bigup_fonctions');
		if (!bigup_sous_repertoires($dir)) {
			return false;
		}

		return $dir;
	}

	/**
@@ -364,10 +262,11 @@ class Flow {
	 *
	 * @uses determine_upload()
	 * @param string $identifier
	 * @param bool $nocreate
	 * @return string chemin du répertoire
	**/
	public function determine_upload_parts($identifier = null) {
		return $this->determine_upload($identifier, 'parts');
	public function determine_upload_parts($identifier = null, $nocreate = false) {
		return $this->determine_upload($identifier, 'parts', $nocreate);
	}


@@ -427,12 +326,15 @@ class Flow {
	/**
	 * Retourne le nom du fichier qui enregistre un des morceaux
	 *
	 * @param string $identifier
	 * @param string $filename
	 * @param int $chunkNumber
	 * @param bool $nocreate
	 *     - true pour ne pas créer le répertoire s'il manque.
	 * @return string Nom de fichier
	**/
	public function tmpChunkPathFile($identifier, $filename, $chunkNumber) {
		return $this->determine_upload_parts($identifier) . $filename . '.part' . $chunkNumber;
	public function tmpChunkPathFile($identifier, $filename, $chunkNumber, $nocreate = false) {
		return $this->determine_upload_parts($identifier, $nocreate) . DIRECTORY_SEPARATOR . $filename . '.part' . $chunkNumber;
	}

	/**
@@ -446,7 +348,7 @@ class Flow {
	 * @return string Nom de fichier
	**/
	public function tmpPathFile($identifier, $filename) {
		return $this->determine_upload_final($identifier) . $filename;
		return $this->determine_upload_final($identifier) . DIRECTORY_SEPARATOR . $filename;
	}

	/**
@@ -458,7 +360,7 @@ class Flow {
	 * @return bool True si présent
	**/
	public function isChunkUploaded($identifier, $filename, $chunkNumber) {
		return file_exists($this->tmpChunkPathFile($identifier, $filename, $chunkNumber));
		return file_exists($this->tmpChunkPathFile($identifier, $filename, $chunkNumber, true));
	}

	/**
@@ -491,13 +393,13 @@ class Flow {
	**/
	public function getChunkFiles($identifier) {
		// Trouver tous les fichiers du répertoire
		$chunkFiles = array_diff(scandir($this->determine_upload_parts($identifier)), array('..', '.', '.ok'));
		$chunkFiles = array_diff(scandir($this->determine_upload_parts($identifier, true)), ['..', '.', '.ok']);

		// Utiliser un chemin complet, et aucun fichier caché.
		$chunkFiles = array_map(
			function ($f) use ($identifier) {
				if ($f and $f[0] != '.') {
					return $this->determine_upload_parts($identifier) . $f;
					return $this->determine_upload_parts($identifier, true) . DIRECTORY_SEPARATOR . $f;
				}
				return '';
			},
@@ -513,6 +415,8 @@ class Flow {
	/**
	 * Recrée le fichier complet à partir des morceaux de fichiers
	 *
	 * Supprime les morceaux si l'opération réussie.
	 * 
	 * @param array $crunkFiles Chemin des morceaux de fichiers à concaténer (dans l'ordre)
	 * @param string $destFile Chemin du fichier à créer avec les morceaux
	 * @return false|string
@@ -525,6 +429,17 @@ class Flow {
			fwrite($fp, file_get_contents($chunkFile));
		}
		fclose($fp);
		return file_exists($destFile) ? $destFile : false;

		if (!file_exists($destFile)) {
			return false;
		}

		$this->info("Fichier complet recréé : " . $destFile);
		$this->debug("Suppression des morceaux.");
		foreach ($chunkFiles as $f) {
			@unlink($f);
		}

		return $destFile;
	}
}
Chargement en cours