init
[garradin.git] / include / class.wiki.php
1 <?php
2
3 namespace Garradin;
4
5 class Wiki
6 {
7 const LECTURE_PUBLIC = -1;
8 const LECTURE_NORMAL = 0;
9 const LECTURE_CATEGORIE = 1;
10
11 const ECRITURE_NORMAL = 0;
12 const ECRITURE_CATEGORIE = 1;
13
14 const ITEMS_PER_PAGE = 25;
15
16 protected $restriction_categorie = null;
17 protected $restriction_droit = null;
18
19 static public function transformTitleToURI($str)
20 {
21 $str = utils::transliterateToAscii($str);
22
23 $str = preg_replace('![^\w\d_-]!i', '-', $str);
24 $str = preg_replace('!-{2,}!', '-', $str);
25 $str = trim($str, '-');
26
27 return $str;
28 }
29
30 // Gestion des données ///////////////////////////////////////////////////////
31
32 public function _checkFields(&$data)
33 {
34 $db = DB::getInstance();
35
36 if (isset($data['titre']) && !trim($data['titre']))
37 {
38 throw new UserException('Le titre ne peut rester vide.');
39 }
40
41 if (isset($data['uri']) && !trim($data['uri']))
42 {
43 throw new UserException('L\'adresse de la page ne peut rester vide.');
44 }
45
46 if (isset($data['droit_lecture']))
47 {
48 $data['droit_lecture'] = (int) $data['droit_lecture'];
49
50 if ($data['droit_lecture'] < -1)
51 {
52 $data['droit_lecture'] = 0;
53 }
54 }
55
56 if (isset($data['droit_ecriture']))
57 {
58 $data['droit_ecriture'] = (int) $data['droit_ecriture'];
59
60 if ($data['droit_ecriture'] < 0)
61 {
62 $data['droit_ecriture'] = 0;
63 }
64 }
65
66 if (isset($data['parent']))
67 {
68 $data['parent'] = (int) $data['parent'];
69
70 if ($data['parent'] < 0)
71 {
72 $data['parent'] = 0;
73 }
74
75 if (!$db->simpleQuerySingle('SELECT 1 FROM wiki_pages WHERE id = ?;', false, $data['parent']))
76 {
77 $data['parent'] = 0;
78 }
79 }
80
81 return true;
82 }
83
84 public function create($data = [])
85 {
86 $this->_checkFields($data);
87 $db = DB::getInstance();
88
89 if (!empty($data['uri']))
90 {
91 $data['uri'] = self::transformTitleToURI($data['uri']);
92
93 if ($db->simpleQuerySingle('SELECT 1 FROM wiki_pages WHERE uri = ? LIMIT 1;', false, $data['uri']))
94 {
95 throw new UserException('Cette adresse de page est déjà utilisée pour une autre page, il faut en choisir une autre.');
96 }
97 }
98 else
99 {
100 $data['uri'] = self::transformTitleToURI($data['titre']);
101
102 if (!trim($data['uri']) || $db->simpleQuerySingle('SELECT 1 FROM wiki_pages WHERE uri = ? LIMIT 1;', false, $data['uri']))
103 {
104 $data['uri'] .= '_' . date('d-m-Y_H-i-s');
105 }
106 }
107
108 $db->simpleInsert('wiki_pages', $data);
109 $id = $db->lastInsertRowId();
110
111 // On ne peut utiliser un trigger pour insérer dans la recherche
112 // car les tables virtuelles font des opérations qui modifient
113 // last_insert_rowid() et donc résultat incohérent
114 $db->simpleInsert('wiki_recherche', ['id' => $id, 'titre' => $data['titre']]);
115
116 return $id;
117 }
118
119 public function edit($id, $data = [])
120 {
121 $db = DB::getInstance();
122 $this->_checkFields($data);
123
124 if (isset($data['uri']))
125 {
126 $data['uri'] = self::transformTitleToURI($data['uri']);
127
128 if ($db->simpleQuerySingle('SELECT 1 FROM wiki_pages WHERE uri = ? AND id != ? LIMIT 1;', false, $data['uri'], (int)$id))
129 {
130 throw new UserException('Cette adresse de page est déjà utilisée pour une autre page, il faut en choisir une autre.');
131 }
132 }
133
134 if (isset($data['droit_lecture']) && $data['droit_lecture'] >= self::LECTURE_CATEGORIE)
135 {
136 $data['droit_ecriture'] = $data['droit_lecture'];
137 }
138
139 if (isset($data['parent']) && (int)$data['parent'] == (int)$id)
140 {
141 $data['parent'] = 0;
142 }
143
144 $data['date_modification'] = gmdate('Y-m-d H:i:s');
145
146 // Modification de la date de création
147 if (isset($data['date_creation']))
148 {
149 // Si la date n'est pas valide tant pis
150 if (!(strtotime($data['date_creation']) > 0))
151 {
152 unset($data['date_creation']);
153 }
154 else
155 {
156 $data['date_creation'] = gmdate('Y-m-d H:i:s', $data['date_creation']);
157 }
158 }
159
160 $db->simpleUpdate('wiki_pages', $data, 'id = '.(int)$id);
161 return true;
162 }
163
164 public function delete($id)
165 {
166 $db = DB::getInstance();
167
168 if ($db->simpleQuerySingle('SELECT COUNT(*) FROM wiki_pages WHERE parent = ?;', false, (int)$id))
169 {
170 return false;
171 }
172
173 $db->simpleExec('DELETE FROM wiki_revisions WHERE id_page = ?;', (int)$id);
174 //$db->simpleExec('DELETE FROM wiki_suivi WHERE id_page = ?;', (int)$id); FIXME
175 $db->simpleExec('DELETE FROM wiki_recherche WHERE id = ?;', (int)$id);
176 $db->simpleExec('DELETE FROM wiki_pages WHERE id = ?;', (int)$id);
177 return true;
178 }
179
180 public function get($id)
181 {
182 $db = DB::getInstance();
183 return $db->simpleQuerySingle('SELECT *,
184 strftime(\'%s\', date_creation) AS date_creation,
185 strftime(\'%s\', date_modification) AS date_modification
186 FROM wiki_pages WHERE id = ? LIMIT 1;', true, (int)$id);
187 }
188
189 public function getTitle($id)
190 {
191 $db = DB::getInstance();
192 return $db->simpleQuerySingle('SELECT titre FROM wiki_pages WHERE id = ? LIMIT 1;', false, (int)$id);
193 }
194
195 public function getRevision($id, $rev)
196 {
197 $db = DB::getInstance();
198 $champ_id = Config::getInstance()->get('champ_identite');
199
200 // FIXME pagination au lieu de bloquer à 1000
201 return $db->simpleQuerySingle('SELECT r.revision, r.modification, r.id_auteur, r.contenu,
202 strftime(\'%s\', r.date) AS date, LENGTH(r.contenu) AS taille, m.'.$champ_id.' AS nom_auteur,
203 r.chiffrement
204 FROM wiki_revisions AS r LEFT JOIN membres AS m ON m.id = r.id_auteur
205 WHERE r.id_page = ? AND revision = ? LIMIT 1;', true, (int) $id, (int) $rev);
206 }
207
208 public function listRevisions($id)
209 {
210 $db = DB::getInstance();
211 $champ_id = Config::getInstance()->get('champ_identite');
212
213 // FIXME pagination au lieu de bloquer à 1000
214 return $db->simpleStatementFetch('SELECT r.revision, r.modification, r.id_auteur,
215 strftime(\'%s\', r.date) AS date, LENGTH(r.contenu) AS taille, m.'.$champ_id.' AS nom_auteur,
216 LENGTH(r.contenu) - (SELECT LENGTH(contenu) FROM wiki_revisions WHERE id_page = r.id_page AND revision < r.revision ORDER BY revision DESC LIMIT 1)
217 AS diff_taille, r.chiffrement
218 FROM wiki_revisions AS r LEFT JOIN membres AS m ON m.id = r.id_auteur
219 WHERE r.id_page = ? ORDER BY r.revision DESC LIMIT 1000;', SQLITE3_ASSOC, (int) $id);
220 }
221
222 public function editRevision($id, $revision_edition = 0, $data)
223 {
224 $db = DB::getInstance();
225
226 $revision = $db->simpleQuerySingle('SELECT revision FROM wiki_pages WHERE id = ?;', false, (int)$id);
227
228 // ?! L'ID fournit ne correspond à rien ?
229 if ($revision === false)
230 {
231 throw new \RuntimeException('La page demandée n\'existe pas.');
232 }
233
234 // Pas de révision
235 if ($revision == 0 && !trim($data['contenu']))
236 {
237 return true;
238 }
239
240 // Il faut obligatoirement fournir un ID d'auteur
241 if (empty($data['id_auteur']) && $data['id_auteur'] !== null)
242 {
243 throw new \BadMethodCallException('Aucun ID auteur de fourni.');
244 }
245
246 $contenu = $db->simpleQuerySingle('SELECT contenu FROM wiki_revisions WHERE revision = ? AND id_page = ?;', false, (int)$revision, (int)$id);
247
248 // Pas de changement au contenu, pas la peine d'enregistrer une nouvelle révision
249 if (trim($contenu) == trim($data['contenu']))
250 {
251 return true;
252 }
253
254 // Révision sur laquelle est basée la nouvelle révision
255 // utilisé pour vérifier que le contenu n'a pas été modifié depuis qu'on
256 // a chargé la page d'édition
257 if ($revision > $revision_edition)
258 {
259 throw new UserException('La page a été modifiée depuis le début de votre modification.');
260 }
261
262 if (empty($data['chiffrement']))
263 $data['chiffrement'] = 0;
264
265 if (!isset($data['modification']) || !trim($data['modification']))
266 $data['modification'] = null;
267
268 // Incrémentons le numéro de révision
269 $revision++;
270
271 $data['id_page'] = $id;
272 $data['revision'] = $revision;
273
274 $db->simpleInsert('wiki_revisions', $data);
275 $db->simpleUpdate('wiki_pages', [
276 'revision' => $revision,
277 'date_modification' => gmdate('Y-m-d H:i:s'),
278 ], 'id = '.(int)$id);
279
280 return true;
281 }
282
283 public function search($query)
284 {
285 $db = DB::getInstance();
286 return $db->simpleStatementFetch('SELECT
287 p.uri, r.*, snippet(wiki_recherche, \'<b>\', \'</b>\', \'...\', -1, -50) AS snippet,
288 rank(matchinfo(wiki_recherche), 0, 1.0, 1.0) AS points
289 FROM wiki_recherche AS r INNER JOIN wiki_pages AS p ON p.id = r.id
290 WHERE '.$this->_getLectureClause('p.').' AND wiki_recherche MATCH \''.$db->escapeString($query).'\'
291 ORDER BY points DESC LIMIT 0,50;');
292 }
293
294 public function setRestrictionCategorie($id, $droit_wiki)
295 {
296 $this->restriction_categorie = $id;
297 $this->restriction_droit = $droit_wiki;
298 return true;
299 }
300
301 protected function _getLectureClause($prefix = '')
302 {
303 if (is_null($this->restriction_categorie))
304 {
305 throw new \UnexpectedValueException('setRestrictionCategorie doit être appelé auparavant.');
306 }
307
308 if ($this->restriction_droit == Membres::DROIT_AUCUN)
309 {
310 throw new UserException('Vous n\'avez pas accès au wiki.');
311 }
312
313 if ($this->restriction_droit == Membres::DROIT_ADMIN)
314 return '1';
315
316 return '('.$prefix.'droit_lecture = '.self::LECTURE_NORMAL.' OR '.$prefix.'droit_lecture = '.self::LECTURE_PUBLIC.'
317 OR '.$prefix.'droit_lecture = '.(int)$this->restriction_categorie.')';
318 }
319
320 public function canReadPage($lecture)
321 {
322 if (is_null($this->restriction_categorie))
323 {
324 throw new \UnexpectedValueException('setRestrictionCategorie doit être appelé auparavant.');
325 }
326
327 if ($this->restriction_droit < Membres::DROIT_ACCES)
328 {
329 return false;
330 }
331
332 if ($this->restriction_droit == Membres::DROIT_ADMIN
333 || $lecture == self::LECTURE_NORMAL || $lecture == self::LECTURE_PUBLIC
334 || $lecture == $this->restriction_categorie)
335 return true;
336
337 return false;
338 }
339
340 public function canWritePage($ecriture)
341 {
342 if (is_null($this->restriction_categorie))
343 {
344 throw new \UnexpectedValueException('setRestrictionCategorie doit être appelé auparavant.');
345 }
346
347 if ($this->restriction_droit < Membres::DROIT_ECRITURE)
348 {
349 return false;
350 }
351
352 if ($this->restriction_droit == Membres::DROIT_ADMIN
353 || $ecriture == self::ECRITURE_NORMAL
354 || $ecriture == $this->restriction_categorie)
355 return true;
356
357 return false;
358 }
359
360 public function getList($parent = 0)
361 {
362 $db = DB::getInstance();
363
364 return $db->simpleStatementFetch(
365 'SELECT id, revision, uri, titre,
366 strftime(\'%s\', date_creation) AS date_creation,
367 strftime(\'%s\', date_modification) AS date_modification
368 FROM wiki_pages
369 WHERE parent = ? AND '.$this->_getLectureClause().'
370 ORDER BY transliterate_to_ascii(titre) COLLATE NOCASE LIMIT 500;',
371 SQLITE3_ASSOC,
372 (int) $parent
373 );
374 }
375
376 public function getById($id)
377 {
378 $db = DB::getInstance();
379 $page = $db->simpleQuerySingle('SELECT *,
380 strftime(\'%s\', date_creation) AS date_creation,
381 strftime(\'%s\', date_modification) AS date_modification
382 FROM wiki_pages
383 WHERE id = ?;', true, (int)$id);
384
385 if (!$page)
386 {
387 return false;
388 }
389
390 if ($page['revision'] > 0)
391 {
392 $page['contenu'] = $db->simpleQuerySingle('SELECT * FROM wiki_revisions
393 WHERE id_page = ? AND revision = ?;', true, (int)$page['id'], (int)$page['revision']);
394 }
395 else
396 {
397 $page['contenu'] = false;
398 }
399
400 return $page;
401 }
402
403 public function getByURI($uri)
404 {
405 $db = DB::getInstance();
406 $page = $db->simpleQuerySingle('SELECT *,
407 strftime(\'%s\', date_creation) AS date_creation,
408 strftime(\'%s\', date_modification) AS date_modification
409 FROM wiki_pages
410 WHERE uri = ?;', true, trim($uri));
411
412 if (!$page)
413 {
414 return false;
415 }
416
417 if ($page['revision'] > 0)
418 {
419 $page['contenu'] = $db->simpleQuerySingle('SELECT * FROM wiki_revisions
420 WHERE id_page = ? AND revision = ?;', true, (int)$page['id'], (int)$page['revision']);
421 }
422 else
423 {
424 $page['contenu'] = false;
425 }
426
427 return $page;
428 }
429
430 public function listRecentModifications($page = 1)
431 {
432 $begin = ($page - 1) * self::ITEMS_PER_PAGE;
433
434 $db = DB::getInstance();
435
436 return $db->simpleStatementFetch('SELECT *,
437 strftime(\'%s\', date_creation) AS date_creation,
438 strftime(\'%s\', date_modification) AS date_modification
439 FROM wiki_pages
440 WHERE '.$this->_getLectureClause().'
441 ORDER BY date_modification DESC;', SQLITE3_ASSOC);
442 }
443
444 public function countRecentModifications()
445 {
446 $db = DB::getInstance();
447 return $db->simpleQuerySingle('SELECT COUNT(*) FROM wiki_pages WHERE '.$this->_getLectureClause().';');
448 }
449
450 public function listBackBreadCrumbs($id)
451 {
452 if ($id == 0)
453 return [];
454
455 $db = DB::getInstance();
456 $flat = [];
457
458 while ($id > 0)
459 {
460 $res = $db->simpleQuerySingle('SELECT parent, titre, uri
461 FROM wiki_pages WHERE id = ? LIMIT 1;', true, (int)$id);
462
463 $flat[] = [
464 'id' => $id,
465 'titre' => $res['titre'],
466 'uri' => $res['uri'],
467 ];
468
469 $id = (int)$res['parent'];
470 }
471
472 return array_reverse($flat);
473 }
474
475 public function listBackParentTree($id)
476 {
477 $db = DB::getInstance();
478 $flat = [
479 [
480 'id' => 0,
481 'parent' => null,
482 'titre' => 'Racine',
483 'children' => $db->simpleStatementFetchAssocKey('SELECT id, parent, titre FROM wiki_pages
484 WHERE parent = ? ORDER BY transliterate_to_ascii(titre) COLLATE NOCASE;',
485 SQLITE3_ASSOC, 0)
486 ]
487 ];
488
489 do
490 {
491 $parent = $db->simpleQuerySingle('SELECT parent FROM wiki_pages WHERE id = ? LIMIT 1;', false, (int)$id);
492
493 $flat[$id] = [
494 'id' => $id,
495 'parent' => $id ? (int)$parent : null,
496 'titre' => $id ? (string)$db->simpleQuerySingle('SELECT titre FROM wiki_pages WHERE id = ? LIMIT 1;', false, (int)$id) : 'Racine',
497 'children' => $db->simpleStatementFetchAssocKey('SELECT id, parent, titre FROM wiki_pages
498 WHERE parent = ? ORDER BY transliterate_to_ascii(titre) COLLATE NOCASE;',
499 SQLITE3_ASSOC, (int)$id)
500 ];
501
502 $id = (int)$parent;
503 }
504 while ($id != 0);
505
506 $tree = [];
507 foreach ($flat as $id=>&$node)
508 {
509 if (is_null($node['parent']))
510 {
511 $tree[$id] = &$node;
512 }
513 else
514 {
515 if (!isset($flat[$node['parent']]['children']))
516 {
517 $flat[$node['parent']]['children'] = [];
518 }
519
520 $flat[$node['parent']]['children'][$id] = &$node;
521 }
522 }
523
524 return $tree;
525 }
526 }
527
528 ?>