Overview
Comment:Amélioration de la vérification de hash pour l'import de DB
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA1: 42d8fe5ab57aa10bb13743194625c2d2ec272da0
User & Date: bohwaz on 2017-05-05 07:35:00
Other Links: branch diff | manifest | tags
References
2017-05-08
07:31 Ticket [f8044779aa] Signature des exports de base de données status still Open with 5 other changes artifact: 0eff8145c3 user: bohwaz
Context
2017-05-08
00:13
Merge avec trunk check-in: 0e42b2b443 user: bohwaz tags: dev
2017-05-05
07:35
Amélioration de la vérification de hash pour l'import de DB check-in: 42d8fe5ab5 user: bohwaz tags: dev
2017-05-04
07:38
Modernisation/corrections du code check-in: abedb2727c user: bohwaz tags: dev
Changes

Modified src/include/lib/Garradin/DB.php from [8264aea346] to [a155d87551].

1
2
3
4
5
6






7
8
9
10
11
12
13
<?php

namespace Garradin;

class DB
{






    static protected $_instance = null;

    /**
     * Instance SQLite3
     * @var SQLite3
     */
    protected $db = null;






>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace Garradin;

class DB
{
    /**
     * Application ID pour SQLite
     * @link https://www.sqlite.org/pragma.html#pragma_application_id
     */
    const APPID = 0x5da2d811;

    static protected $_instance = null;

    /**
     * Instance SQLite3
     * @var SQLite3
     */
    protected $db = null;

Modified src/include/lib/Garradin/Install.php from [cbbaf6e1e8] to [323e87e4cd].

10
11
12
13
14
15
16

17
18
19
20
21
22
23
{
	static public function install($nom_asso, $adresse_asso, $email_asso, $nom_categorie, $nom_membre, $email_membre, $passe_membre, $site_asso = WWW_URL)
	{
		$db = DB::getInstance(true);

		// Création de la base de données
		$db->begin();

		$db->exec(file_get_contents(DB_SCHEMA));
		$db->commit();

		// Configuration de base
		// c'est dans Config::set que sont vérifiées les données utilisateur (renvoie UserException)
		$config = Config::getInstance();
		$config->set('nom_asso', $nom_asso);







>







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
	static public function install($nom_asso, $adresse_asso, $email_asso, $nom_categorie, $nom_membre, $email_membre, $passe_membre, $site_asso = WWW_URL)
	{
		$db = DB::getInstance(true);

		// Création de la base de données
		$db->begin();
		$db->exec('PRAGMA application_id = ' . DB::APPID . ';');
		$db->exec(file_get_contents(DB_SCHEMA));
		$db->commit();

		// Configuration de base
		// c'est dans Config::set que sont vérifiées les données utilisateur (renvoie UserException)
		$config = Config::getInstance();
		$config->set('nom_asso', $nom_asso);

Modified src/include/lib/Garradin/Sauvegarde.php from [c9ec5ef48e] to [b0e237675e].

147
148
149
150
151
152
153




154
155
156
157
158
159
160
...
173
174
175
176
177
178
179
180



181
182
183
184
185
186
187
188














189
190
191
192
193
194
195
...
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
...
223
224
225
226
227
228
229
230
231
232
233
234
235


236
237

238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
...
303
304
305
306
307
308
309
310
311
312
313
314
315














316
317
318
319
320
321
322
...
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339

        while (!feof($in))
        {
        	fwrite($out, fread($in, 8192));
        }

        fclose($in);




        fclose($out);
        return true;
	}

	/**
	 * Restaure une sauvegarde locale
	 * @param  string $file Le nom de fichier à utiliser comme point de restauration
................................................................................
		}

		return $this->restoreDB(DATA_ROOT . '/' . $file);
	}

	/**
	 * Restaure une copie distante (fichier envoyé)
	 * @param  array  $file Tableau provenant de $_FILES



	 * @return boolean true
	 */
	public function restoreFromUpload($file, $user_id)
	{
		if (empty($file['size']) || empty($file['tmp_name']) || !empty($file['error']))
		{
			throw new UserException('Le fichier n\'a pas été correctement envoyé. Essayer de le renvoyer à nouveau.');
		}















		$r = $this->restoreDB($file['tmp_name'], $user_id);

		if ($r)
		{
			unlink($file['tmp_name']);
		}
................................................................................
	 * Vérifie l'intégrité d'une sauvegarde Garradin
	 * @param  string $file Chemin absolu vers la base de donnée
	 * @return boolean
	 */
	protected function checkIntegrity($file_path, $remove_hash = true)
	{
		$size = filesize($file_path);
		$fp = fopen($file_path, 'r');

		$header = fread($fp, 16);

		// Vérifie que le fichier est bien une base SQLite3
		if ($header !== "SQLite format 3\000")
		{
			fclose($fp);
................................................................................
		// Ne ressemble pas à un hash sha1
		if (!preg_match('/[a-f0-9]{40}/', $hash))
		{
			fclose($fp);
			return false;
		}

		fseek($fp, 0);

		$content = '';
		$max = $size - 40;

		while (!feof($fp) && strlen($file) < $max)


		{
			$content .= fread($fp, 4096);

		}

		fclose($fp);

		$file_hash = sha1($content);

		// Vérification du hash
		if ($file_hash === $hash)
		{
			// Suppression du hash
			if ($remove_hash)
			{
				file_put_contents($file_path, $content);
			}

			return true;
		}

		return false;
	}

	/**
	 * Restauration de base de données, la fonction qui le fait vraiment
	 * @param  string $file Chemin absolu vers la base de données à utiliser
	 * @return mixed 		true si rien ne va plus, ou self::NEED_UPGRADE si la version de la DB
	 * ne correspond pas à la version de Garradin (mise à jour nécessaire).
	 */
	protected function restoreDB($file, $user_id = false, $check_integrity = true)
	{
		if ($check_integrity)
		{
			$integrity = $this->checkIntegrity($file);

			if ($integrity === null)
			{
				throw new UserException('Le fichier fourni n\'est pas une base de donnée SQLite3.');
			}
			elseif ($integrity === false)
			{
				return self::INTEGRITY_FAIL;
			}
		}

		// Essayons déjà d'ouvrir la base de données à restaurer en lecture
		try {
			$db = new \SQLite3($file, SQLITE3_OPEN_READONLY);
		}
		catch (\Exception $e)
		{
			throw new UserException('Le fichier fourni n\'est pas une base de données valide. ' .
				'Message d\'erreur de SQLite : ' . $e->getMessage());
		}

		try {
			// Regardons ensuite si la base de données n'est pas corrompue
			$check = $db->querySingle('PRAGMA integrity_check;');
		}
		catch (\Exception $e)
		{
			// Ici SQLite peut rejeter un message type "file is encrypted or is not a db"
			throw new UserException('Le fichier fourni n\'est pas une base de données valide. ' .
				'Message d\'erreur de SQLite : ' . $e->getMessage());
		}
................................................................................
		{
			throw new UserException('Le fichier fourni est corrompu. SQLite a trouvé ' . $check . ' erreurs.');
		}

		// On ne peut pas faire de vérifications très poussées sur la structure de la base de données,
		// celle-ci pouvant changer d'une version à l'autre et on peut vouloir importer une base
		// un peu vieille, mais on vérifie quand même que ça ressemble un minimum à une base garradin
		$table = $db->querySingle('SELECT 1 FROM sqlite_master WHERE type=\'table\' AND tbl_name=\'config\';');

		if (!$table)
		{
			throw new UserException('Le fichier fourni ne semble pas contenir de données liées à Garradin.');
		}















		if ($user_id)
		{
			// Empêchons l'admin de se tirer une balle dans le pied
			$is_still_admin = $db->querySingle('SELECT 1 FROM membres_categories 
				WHERE id = (SELECT id_categorie FROM membres WHERE id = ' . (int) $user_id . ')
				AND droit_config >= ' . Membres::DROIT_ADMIN . '
................................................................................

			if (!$is_still_admin)
			{
				throw new UserException('Vous n\'êtes pas administrateur dans le fichier de sauvegarde fourni.');
			}
		}

		// On récupère la version pour plus tard
		$version = $db->querySingle('SELECT valeur FROM config WHERE cle=\'version\';');

		$db->close();

		$backup = str_replace('.sqlite', date('.Y-m-d-His') . '.avant_restauration.sqlite', DB_FILE);
		
		if (!rename(DB_FILE, $backup))
		{







>
>
>
>







 







|
>
>
>


|





>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







|







 







<
<
<


<
>
>

<
>




|


|
<
<
<
<
<
<
<
<
<
<
<








|

<
<
<
<
<
<
<
<
<
<
<
<
<
<












|







 







|





>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







<
<







147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
...
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
...
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
...
244
245
246
247
248
249
250



251
252

253
254
255

256
257
258
259
260
261
262
263
264











265
266
267
268
269
270
271
272
273
274














275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
...
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
...
332
333
334
335
336
337
338


339
340
341
342
343
344
345

        while (!feof($in))
        {
        	fwrite($out, fread($in, 8192));
        }

        fclose($in);

        // Ajout du hash pour vérification intégrité
        fwrite($out, sha1_file(DB_FILE));

        fclose($out);
        return true;
	}

	/**
	 * Restaure une sauvegarde locale
	 * @param  string $file Le nom de fichier à utiliser comme point de restauration
................................................................................
		}

		return $this->restoreDB(DATA_ROOT . '/' . $file);
	}

	/**
	 * Restaure une copie distante (fichier envoyé)
	 * @param  array   $file    Tableau provenant de $_FILES
	 * @param  integer $user_id ID du membre actuellement connecté, utilisé pour 
	 * vérifier qu'il est toujours administrateur dans la sauvegarde
	 * @param  boolean $check_integrity Vérifier l'intégrité de la sauvegarde avant de restaurer
	 * @return boolean true
	 */
	public function restoreFromUpload($file, $user_id, $check_integrity = true)
	{
		if (empty($file['size']) || empty($file['tmp_name']) || !empty($file['error']))
		{
			throw new UserException('Le fichier n\'a pas été correctement envoyé. Essayer de le renvoyer à nouveau.');
		}

		if ($check_integrity)
		{
			$integrity = $this->checkIntegrity($file['tmp_name']);

			if ($integrity === null)
			{
				throw new UserException('Le fichier fourni n\'est pas une base de donnée SQLite3.');
			}
			elseif ($integrity === false)
			{
				return self::INTEGRITY_FAIL;
			}
		}

		$r = $this->restoreDB($file['tmp_name'], $user_id);

		if ($r)
		{
			unlink($file['tmp_name']);
		}
................................................................................
	 * Vérifie l'intégrité d'une sauvegarde Garradin
	 * @param  string $file Chemin absolu vers la base de donnée
	 * @return boolean
	 */
	protected function checkIntegrity($file_path, $remove_hash = true)
	{
		$size = filesize($file_path);
		$fp = fopen($file_path, 'r+');

		$header = fread($fp, 16);

		// Vérifie que le fichier est bien une base SQLite3
		if ($header !== "SQLite format 3\000")
		{
			fclose($fp);
................................................................................
		// Ne ressemble pas à un hash sha1
		if (!preg_match('/[a-f0-9]{40}/', $hash))
		{
			fclose($fp);
			return false;
		}




		$max = $size - 40;


		// Suppression du hash
		if ($remove_hash)
		{

			ftruncate($fp, $max);
		}

		fclose($fp);

		$file_hash = sha1_file($file_path);

		// Vérification du hash
		return ($file_hash === $hash);











	}

	/**
	 * Restauration de base de données, la fonction qui le fait vraiment
	 * @param  string $file Chemin absolu vers la base de données à utiliser
	 * @return mixed 		true si rien ne va plus, ou self::NEED_UPGRADE si la version de la DB
	 * ne correspond pas à la version de Garradin (mise à jour nécessaire).
	 */
	protected function restoreDB($file, $user_id = false)
	{














		// Essayons déjà d'ouvrir la base de données à restaurer en lecture
		try {
			$db = new \SQLite3($file, SQLITE3_OPEN_READONLY);
		}
		catch (\Exception $e)
		{
			throw new UserException('Le fichier fourni n\'est pas une base de données valide. ' .
				'Message d\'erreur de SQLite : ' . $e->getMessage());
		}

		try {
			// Regardons ensuite si la base de données n'est pas corrompue
			$check = $db->firstColumn('PRAGMA integrity_check;');
		}
		catch (\Exception $e)
		{
			// Ici SQLite peut rejeter un message type "file is encrypted or is not a db"
			throw new UserException('Le fichier fourni n\'est pas une base de données valide. ' .
				'Message d\'erreur de SQLite : ' . $e->getMessage());
		}
................................................................................
		{
			throw new UserException('Le fichier fourni est corrompu. SQLite a trouvé ' . $check . ' erreurs.');
		}

		// On ne peut pas faire de vérifications très poussées sur la structure de la base de données,
		// celle-ci pouvant changer d'une version à l'autre et on peut vouloir importer une base
		// un peu vieille, mais on vérifie quand même que ça ressemble un minimum à une base garradin
		$table = $db->firstColumn('SELECT 1 FROM sqlite_master WHERE type=\'table\' AND tbl_name=\'config\';');

		if (!$table)
		{
			throw new UserException('Le fichier fourni ne semble pas contenir de données liées à Garradin.');
		}

		// On récupère la version
		$version = $db->querySingle('SELECT valeur FROM config WHERE cle=\'version\';');

		// Vérification de l'AppID pour les versions récentes
		if (version_compare($version, '0.8.0', '>='))
		{
			$appid = $db->firstColumn('PRAGMA application_id;');

			if ($appid !== DB::APPID)
			{
				throw new UserException('Ce fichier n\'est pas une sauvegarde Garradin (AppID ne correspond pas).');
			}
		}

		if ($user_id)
		{
			// Empêchons l'admin de se tirer une balle dans le pied
			$is_still_admin = $db->querySingle('SELECT 1 FROM membres_categories 
				WHERE id = (SELECT id_categorie FROM membres WHERE id = ' . (int) $user_id . ')
				AND droit_config >= ' . Membres::DROIT_ADMIN . '
................................................................................

			if (!$is_still_admin)
			{
				throw new UserException('Vous n\'êtes pas administrateur dans le fichier de sauvegarde fourni.');
			}
		}




		$db->close();

		$backup = str_replace('.sqlite', date('.Y-m-d-His') . '.avant_restauration.sqlite', DB_FILE);
		
		if (!rename(DB_FILE, $backup))
		{

Modified src/www/admin/upgrade.php from [80018cd596] to [db73c7b27f].

247
248
249
250
251
252
253



254
255
256
257
258
259
260
if (version_compare($v, '0.8.0', '<'))
{
    $db->exec('PRAGMA foreign_keys = OFF; BEGIN;');

    // Mise à jour base de données
    $db->exec(file_get_contents(ROOT . '/include/data/0.8.0.sql'));




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

Utils::clearCaches();

$config->setVersion(garradin_version());








>
>
>







247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
if (version_compare($v, '0.8.0', '<'))
{
    $db->exec('PRAGMA foreign_keys = OFF; BEGIN;');

    // Mise à jour base de données
    $db->exec(file_get_contents(ROOT . '/include/data/0.8.0.sql'));

    // Inscriptin de l'appid
    $db->exec('PRAGMA application_id = ' . DB::APPID . ';');

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

Utils::clearCaches();

$config->setVersion(garradin_version());