[SPIP] v3.2.7-->v3.2.9
[lhc/web/www.git] / www / ecrire / inc / editer.php
1 <?php
2
3 /***************************************************************************\
4 * SPIP, Systeme de publication pour l'internet *
5 * *
6 * Copyright (c) 2001-2019 *
7 * Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James *
8 * *
9 * Ce programme est un logiciel libre distribue sous licence GNU/GPL. *
10 * Pour plus de details voir le fichier COPYING.txt ou l'aide en ligne. *
11 \***************************************************************************/
12
13 /**
14 * Fonctions d'aide à l'édition d'objets éditoriaux.
15 *
16 * @package SPIP\Core\Edition
17 **/
18
19 if (!defined('_ECRIRE_INC_VERSION')) {
20 return;
21 }
22 include_spip('base/abstract_sql');
23
24 /**
25 * Effectue les traitements d'un formulaire d'édition d'objet éditorial
26 *
27 * Exécute une action d'édition spécifique au type d'objet s'il elle existe
28 * (fonction action_editer_$type), sinon exécute l'action générique
29 * d'édition d'objet (action_editer_objet_dist())
30 *
31 * Si une traduction était demandée, crée le lien avec l'objet qui est
32 * traduit.
33 *
34 * @api
35 * @see action_editer_objet_dist()
36 *
37 * @param string $type
38 * Type d'objet
39 * @param int|string $id
40 * Identifiant de l'objet à éditer, 'new' pour un nouvel objet
41 * @param int $id_parent
42 * Identifiant de l'objet parent
43 * @param int $lier_trad
44 * Identifiant de l'objet servant de source à une nouvelle traduction
45 * @param string $retour
46 * URL de redirection après les traitements
47 * @param string $config_fonc
48 * Nom de fonction appelée au chargement permettant d'ajouter des
49 * valeurs de configurations dans l'environnement du formulaire
50 * @param array $row
51 * Ligne SQL de l'objet édité, si connu.
52 * En absence, les données sont chargées depuis l'objet en base s'il existe
53 * ou depuis l'objet source d'une traduction si c'est un nouvel objet
54 * (et une traduction).
55 * @param string $hidden
56 * Contenu HTML ajouté en même temps que les champs cachés (input hidden)
57 * du formulaire.
58 * @return array
59 * Retour des traitements.
60 **/
61 function formulaires_editer_objet_traiter(
62 $type,
63 $id = 'new',
64 $id_parent = 0,
65 $lier_trad = 0,
66 $retour = '',
67 $config_fonc = 'articles_edit_config',
68 $row = array(),
69 $hidden = ''
70 ) {
71
72 $res = array();
73 // eviter la redirection forcee par l'action...
74 set_request('redirect');
75 if ($action_editer = charger_fonction("editer_$type", 'action', true)) {
76 list($id, $err) = $action_editer($id);
77 } else {
78 $action_editer = charger_fonction('editer_objet', 'action');
79 list($id, $err) = $action_editer($id, $type);
80 }
81 $id_table_objet = id_table_objet($type);
82 $res[$id_table_objet] = $id;
83 if ($err or !$id) {
84 $res['message_erreur'] = ($err ? $err : _T('erreur'));
85 } else {
86 // Un lien de trad a prendre en compte
87 if ($lier_trad) {
88 // referencer la traduction
89 $referencer_traduction = charger_fonction('referencer_traduction', 'action');
90 $referencer_traduction($type, $id, $lier_trad);
91 // actions de recopie de champs / liens sur le nouvel objet créé
92 $completer_traduction = charger_fonction('completer_traduction', 'inc');
93 $err = $completer_traduction($type, $id, $lier_trad);
94 if ($err) {
95 $res['message_erreur'] = $err;
96 return $res;
97 }
98 }
99
100 $res['message_ok'] = _T('info_modification_enregistree');
101 if ($retour) {
102 if (strncmp($retour, 'javascript:', 11) == 0) {
103 $res['message_ok'] .= '<script type="text/javascript">/*<![CDATA[*/' . substr($retour, 11) . '/*]]>*/</script>';
104 $res['editable'] = true;
105 } else {
106 $res['redirect'] = parametre_url($retour, $id_table_objet, $id);
107 }
108 }
109 }
110
111 return $res;
112 }
113
114 /**
115 * Teste les erreurs de validation d'un formulaire d'édition d'objet éditorial
116 *
117 * La fonction teste que :
118 * - il n'y a pas de conflit d'édition sur un ou plusieurs champs (c'est à
119 * dire que personne d'autre n'a modifié le champ entre le moment où on
120 * a saisi et le moment où on a validé le formulaire
121 * - tous les champs obligatoires (listés dans $oblis) sont remplis.
122 *
123 * @api
124 *
125 * @param string $type
126 * Type d'objet
127 * @param int|string $id
128 * Identifiant de l'objet à éditer, 'new' pour un nouvel objet
129 * @param array $oblis
130 * Liste de champs obligatoires : ils doivent avoir un contenu posté.
131 * @return array
132 * Tableau des erreurs
133 **/
134 function formulaires_editer_objet_verifier($type, $id = 'new', $oblis = array()) {
135 $erreurs = array();
136 if (intval($id)) {
137 $conflits = controler_contenu($type, $id);
138 if ($conflits and count($conflits)) {
139 foreach ($conflits as $champ => $conflit) {
140 if (!isset($erreurs[$champ])) {
141 $erreurs[$champ] = '';
142 }
143 $erreurs[$champ] .= _T('alerte_modif_info_concourante') . "<br /><textarea readonly='readonly' class='forml'>" . entites_html($conflit['base']) . '</textarea>';
144 }
145 }
146 }
147 foreach ($oblis as $obli) {
148 $value = _request($obli);
149 if (is_null($value) or !(is_array($value) ? count($value) : strlen($value))) {
150 if (!isset($erreurs[$obli])) {
151 $erreurs[$obli] = '';
152 }
153 $erreurs[$obli] .= _T('info_obligatoire');
154 }
155 }
156
157 return $erreurs;
158 }
159
160 /**
161 * Construit les valeurs de chargement d'un formulaire d'édition d'objet éditorial
162 *
163 * La fonction calcule les valeurs qui seront transmises à l'environnement
164 * du formulaire pour son affichage. Ces valeurs sont les champs de l'objet
165 * éditorial d'une part, mais aussi d'autres calculant la clé d'action,
166 * les pipelines devant faire transiter le contenu HTML du formulaire,
167 * ainsi que différents champs cachés utilisés ensuite dans les traitements.
168 *
169 * Lorsqu'une création d'objet est demandée, ou lorsqu'on demande une traduction
170 * d'un autre, la fonction tente de précharger le contenu de l'objet en
171 * utilisant une fonction inc_precharger_{type}_dist permettant par exemple
172 * de remplir le contenu avec du texte, notamment avec la traduction source.
173 *
174 * @api
175 *
176 * @param string $type
177 * Type d'objet
178 * @param int|string $id
179 * Identifiant de l'objet à éditer, 'new' pour un nouvel objet
180 * @param int $id_parent
181 * Identifiant de l'objet parent
182 * @param int $lier_trad
183 * Identifiant de l'objet servant de source à une nouvelle traduction
184 * @param string $retour
185 * URL de redirection après les traitements
186 * @param string $config_fonc
187 * Nom de fonction appelée au chargement permettant d'ajouter des
188 * valeurs de configurations dans l'environnement du formulaire
189 * @param array $row
190 * Ligne SQL de l'objet édité, si connu.
191 * En absence, les données sont chargées depuis l'objet en base s'il existe
192 * ou depuis l'objet source d'une traduction si c'est un nouvel objet
193 * (et une traduction).
194 * @param string $hidden
195 * Contenu HTML ajouté en même temps que les champs cachés (input hidden)
196 * du formulaire.
197 * @return array
198 * Environnement du formulaire.
199 **/
200 function formulaires_editer_objet_charger(
201 $type,
202 $id = 'new',
203 $id_parent = 0,
204 $lier_trad = 0,
205 $retour = '',
206 $config_fonc = 'articles_edit_config',
207 $row = array(),
208 $hidden = ''
209 ) {
210
211 $table_objet = table_objet($type);
212 $table_objet_sql = table_objet_sql($type);
213 $id_table_objet = id_table_objet($type);
214
215 // on accepte pas une fonction de config inconnue si elle vient d'un modele
216 if ($config_fonc
217 and !in_array($config_fonc, ['articles_edit_config', 'rubriques_edit_config', 'auteurs_edit_config'])
218 and $config_fonc !== $table_objet . '_edit_config') {
219 if ($args = test_formulaire_inclus_par_modele()
220 and in_array($config_fonc, $args)) {
221 $config_fonc = '';
222 }
223 }
224
225 $new = !is_numeric($id);
226 // Appel direct dans un squelette
227 if (!$row) {
228 if (!$new or $lier_trad) {
229 if ($select = charger_fonction('precharger_' . $type, 'inc', true)) {
230 $row = $select($id, $id_parent, $lier_trad);
231 } else {
232 $row = sql_fetsel('*', $table_objet_sql, $id_table_objet . '=' . intval($id));
233 }
234 if (!$new) {
235 $md5 = controles_md5($row);
236 }
237 }
238 if (!$row) {
239 $row = array();
240 $trouver_table = charger_fonction('trouver_table', 'base');
241 if ($desc = $trouver_table($table_objet)) {
242 foreach ($desc['field'] as $k => $v) {
243 $row[$k] = '';
244 }
245 }
246 }
247 }
248
249 // Gaffe: sans ceci, on ecrase systematiquement l'article d'origine
250 // (et donc: pas de lien de traduction)
251 $id = ($new or $lier_trad)
252 ? 'oui'
253 : $row[$id_table_objet];
254 $row[$id_table_objet] = $id;
255
256 $contexte = $row;
257 if (strlen($id_parent) && is_numeric($id_parent) && (!isset($contexte['id_parent']) or $new)) {
258 if (!isset($contexte['id_parent'])) {
259 unset($contexte['id_rubrique']);
260 }
261 $contexte['id_parent'] = $id_parent;
262 } elseif (!isset($contexte['id_parent'])) {
263 // id_rubrique dans id_parent si possible
264 if (isset($contexte['id_rubrique'])) {
265 $contexte['id_parent'] = $contexte['id_rubrique'];
266 unset($contexte['id_rubrique']);
267 } else {
268 $contexte['id_parent'] = '';
269 }
270 if (!$contexte['id_parent']
271 and $preselectionner_parent_nouvel_objet = charger_fonction('preselectionner_parent_nouvel_objet', 'inc', true)
272 ) {
273 $contexte['id_parent'] = $preselectionner_parent_nouvel_objet($type, $row);
274 }
275 }
276
277 $config = array();
278 if ($config_fonc) {
279 $contexte['config'] = $config = $config_fonc($contexte);
280 }
281 $config = $config + array(
282 'lignes' => 0,
283 'langue' => '',
284 );
285
286 $att_text = " class='textarea' "
287 . " rows='"
288 . ($config['lignes'] + 15)
289 . "' cols='40'";
290 if (isset($contexte['texte'])) {
291 list($contexte['texte'], $contexte['_texte_trop_long']) = editer_texte_recolle($contexte['texte'], $att_text);
292 }
293
294 // on veut conserver la langue de l'interface ;
295 // on passe cette donnee sous un autre nom, au cas ou le squelette
296 // voudrait l'exploiter
297 if (isset($contexte['lang'])) {
298 $contexte['langue'] = $contexte['lang'];
299 unset($contexte['lang']);
300 }
301
302 $contexte['_hidden'] = "<input type='hidden' name='editer_$type' value='oui' />\n" .
303 (!$lier_trad ? '' :
304 ("\n<input type='hidden' name='lier_trad' value='" .
305 $lier_trad .
306 "' />" .
307 "\n<input type='hidden' name='changer_lang' value='" .
308 $config['langue'] .
309 "' />"))
310 . $hidden
311 . (isset($md5) ? $md5 : '');
312
313 // preciser que le formulaire doit passer dans un pipeline
314 $contexte['_pipeline'] = array('editer_contenu_objet', array('type' => $type, 'id' => $id));
315
316 // preciser que le formulaire doit etre securise auteur/action
317 // n'est plus utile lorsque l'action accepte l'id en argument direct
318 // on le garde pour compat
319 $contexte['_action'] = array("editer_$type", $id);
320
321 return $contexte;
322 }
323
324 /**
325 * Gestion des textes trop longs (limitation brouteurs)
326 * utile pour les textes > 32ko
327 *
328 * @param string $texte
329 * @return array
330 */
331 function coupe_trop_long($texte) {
332 $aider = charger_fonction('aider', 'inc');
333 if (strlen($texte) > 28 * 1024) {
334 $texte = str_replace("\r\n", "\n", $texte);
335 $pos = strpos($texte, "\n\n", 28 * 1024); // coupe para > 28 ko
336 if ($pos > 0 and $pos < 32 * 1024) {
337 $debut = substr($texte, 0, $pos) . "\n\n<!--SPIP-->\n";
338 $suite = substr($texte, $pos + 2);
339 } else {
340 $pos = strpos($texte, ' ', 28 * 1024); // sinon coupe espace
341 if (!($pos > 0 and $pos < 32 * 1024)) {
342 $pos = 28 * 1024; // au pire (pas d'espace trouv'e)
343 $decalage = 0; // si y'a pas d'espace, il ne faut pas perdre le caract`ere
344 } else {
345 $decalage = 1;
346 }
347 $debut = substr($texte, 0, $pos + $decalage); // Il faut conserver l'espace s'il y en a un
348 $suite = substr($texte, $pos + $decalage);
349 }
350
351 return (array($debut, $suite));
352 } else {
353 return (array($texte, ''));
354 }
355 }
356
357 /**
358 * Formater un `$texte` dans `textarea`
359 *
360 * @param string $texte
361 * @param string $att_text
362 * @return array
363 */
364 function editer_texte_recolle($texte, $att_text) {
365 if ((strlen($texte) < 29 * 1024)
366 or (include_spip('inc/layer') and ($GLOBALS['browser_name'] != 'MSIE'))
367 ) {
368 return array($texte, '');
369 }
370
371 include_spip('inc/barre');
372 $textes_supplement = "<br /><span style='color: red'>" . _T('info_texte_long') . "</span>\n";
373 $nombre = 0;
374
375 while (strlen($texte) > 29 * 1024) {
376 $nombre++;
377 list($texte1, $texte) = coupe_trop_long($texte);
378 $textes_supplement .= '<br />' .
379 "<textarea id='texte$nombre' name='texte_plus[$nombre]'$att_text>$texte1</textarea>\n";
380 }
381
382 return array($texte, $textes_supplement);
383 }
384
385 /**
386 * auto-renseigner le titre si il n'existe pas
387 *
388 * @param $champ_titre
389 * @param $champs_contenu
390 * @param int $longueur
391 */
392 function titre_automatique($champ_titre, $champs_contenu, $longueur = null) {
393 if (!_request($champ_titre)) {
394 $titrer_contenu = charger_fonction('titrer_contenu', 'inc');
395 if (!is_null($longueur)) {
396 $t = $titrer_contenu($champs_contenu, null, $longueur);
397 } else {
398 $t = $titrer_contenu($champs_contenu);
399 }
400 if ($t) {
401 set_request($champ_titre, $t);
402 }
403 }
404 }
405
406 /**
407 * Déterminer un titre automatique,
408 * à partir des champs textes de contenu
409 *
410 * Les textes et le titre sont pris dans les champs postés (via `_request()`)
411 * et le titre calculé est de même affecté en tant que champ posté.
412 *
413 * @param array $champs_contenu
414 * Liste des champs contenu textuels
415 * @param array|null $c
416 * tableau qui contient les valeurs des champs de contenu
417 * si `null` on utilise les valeurs du POST
418 * @param int $longueur
419 * Longueur de coupe du texte
420 * @return string
421 */
422 function inc_titrer_contenu_dist($champs_contenu, $c = null, $longueur = 50) {
423 // trouver un champ texte non vide
424 $t = '';
425 foreach ($champs_contenu as $champ) {
426 if ($t = _request($champ, $c)) {
427 break;
428 }
429 }
430
431 if ($t) {
432 include_spip('inc/texte_mini');
433 $t = couper($t, $longueur, '...');
434 }
435
436 return $t;
437 }
438
439 /**
440 * Calcule des clés de contrôles md5 d'un tableau de données.
441 *
442 * Produit la liste des md5 d'un tableau de données, normalement un
443 * tableau des colonnes/valeurs d'un objet éditorial.
444 *
445 * @param array $data
446 * Couples (colonne => valeur). La valeur est un entier ou un texte.
447 * @param string $prefixe
448 * Préfixe à appliquer sur les noms des clés de contrôles, devant le
449 * nom de la colonne
450 * @param string $format
451 * - html : Retourne les contrôles sous forme de input hidden pour un formulaire
452 * - autre : Retourne le tableau ('$prefixe$colonne => md5)
453 * @return bool|string|array
454 * - false si pas $data n'est pas un tableau
455 * - string (avec format html) : contrôles dans des input hidden
456 * - array sinon couples ('$prefixe$colonne => md5)
457 **/
458 function controles_md5($data, $prefixe = 'ctr_', $format = 'html') {
459 if (!is_array($data)) {
460 return false;
461 }
462
463 $ctr = array();
464 foreach ($data as $key => $val) {
465 $m = md5($val);
466 $k = $prefixe . $key;
467
468 switch ($format) {
469 case 'html':
470 $ctr[$k] = "<input type='hidden' value='$m' name='$k' />";
471 break;
472 default:
473 $ctr[$k] = $m;
474 break;
475 }
476 }
477
478 if ($format == 'html') {
479 return "\n\n<!-- controles md5 -->\n" . join("\n", $ctr) . "\n\n";
480 } else {
481 return $ctr;
482 }
483 }
484
485 /**
486 * Contrôle les contenus postés d'un objet en vérifiant qu'il n'y a pas
487 * de conflit d'édition
488 *
489 * Repère les conflits d'édition sur un ou plusieurs champs. C'est à
490 * dire lorsqu'une autre personne a modifié le champ entre le moment où on
491 * a édité notre formulaire et le moment où on a validé le formulaire
492 *
493 * @param string $type
494 * Type d'objet
495 * @param int $id
496 * Identifiant de l'objet
497 * @param array $options
498 * Tableau d'options. Accèpte les index :
499 * - nonvide : Couples (colonne => valeur par défaut). Tous les champs
500 * postés qui sont vides, s'il y en a dans cette option, sont remplacés
501 * par la valeur indiquée
502 * - prefix : Préfixe des clés de contrôles ('ctr_' par défaut). Une clé
503 * de controle tel que 'ctr_titre' contient le md5 du titre au moment
504 * de l'édition.
505 * @param array|bool $c
506 * Tableau de couples (colonne=>valeur) à tester.
507 * Non renseigné, la fonction prend toutes les colonne de l'objet via
508 * _request()
509 * @param string $serveur
510 * Nom du connecteur de base de données
511 * @return bool|null|array
512 * False si aucun champ posté.
513 * Null si aucune modification sur les champs.
514 * Tableau vide si aucun de conflit d'édition.
515 * Tableau (clé => tableau du conflit). L'index est la colonne en conflit,
516 * la valeur un tableau avec 2 index :
517 * - base : le contenu du champ en base
518 * - post : le contenu posté
519 **/
520 function controler_contenu($type, $id, $options = array(), $c = false, $serveur = '') {
521 include_spip('inc/filtres');
522
523 $table_objet = table_objet($type);
524 $spip_table_objet = table_objet_sql($type);
525 $trouver_table = charger_fonction('trouver_table', 'base');
526 $desc = $trouver_table($table_objet, $serveur);
527
528 // Appels incomplets (sans $c)
529 if (!is_array($c)) {
530 foreach ($desc['field'] as $champ => $ignore) {
531 if (_request($champ)) {
532 $c[$champ] = _request($champ);
533 }
534 }
535 }
536
537 // Securite : certaines variables ne sont jamais acceptees ici
538 // car elles ne relevent pas de autoriser(article, modifier) ;
539 // il faut passer par instituer_XX()
540 // TODO: faut-il passer ces variables interdites
541 // dans un fichier de description separe ?
542 unset($c['statut']);
543 unset($c['id_parent']);
544 unset($c['id_rubrique']);
545 unset($c['id_secteur']);
546
547 // Gerer les champs non vides
548 if (isset($options['nonvide']) and is_array($options['nonvide'])) {
549 foreach ($options['nonvide'] as $champ => $sinon) {
550 if ($c[$champ] === '') {
551 $c[$champ] = $sinon;
552 }
553 }
554 }
555
556 // N'accepter que les champs qui existent
557 // [TODO] ici aussi on peut valider les contenus en fonction du type
558 $champs = array();
559 foreach ($desc['field'] as $champ => $ignore) {
560 if (isset($c[$champ])) {
561 $champs[$champ] = $c[$champ];
562 }
563 }
564
565 // Nettoyer les valeurs
566 $champs = array_map('corriger_caracteres', $champs);
567
568 // Envoyer aux plugins
569 $champs = pipeline(
570 'pre_edition',
571 array(
572 'args' => array(
573 'table' => $spip_table_objet, // compatibilite
574 'table_objet' => $table_objet,
575 'spip_table_objet' => $spip_table_objet,
576 'type' => $type,
577 'id_objet' => $id,
578 'champs' => isset($options['champs']) ? $options['champs'] : array(), // [doc] c'est quoi ?
579 'action' => 'controler',
580 'serveur' => $serveur,
581 ),
582 'data' => $champs
583 )
584 );
585
586 if (!$champs) {
587 return false;
588 }
589
590 // Verifier si les mises a jour sont pertinentes, datees, en conflit etc
591 $conflits = controler_md5($champs, $_POST, $type, $id, $serveur, isset($options['prefix']) ? $options['prefix'] : 'ctr_');
592
593 return $conflits;
594 }
595
596
597 /**
598 * Contrôle la liste des md5 envoyés, supprime les inchangés,
599 * signale les modifiés depuis telle date
600 *
601 * @param array $champs
602 * Couples des champs saisis dans le formulaire (colonne => valeur postée)
603 * @param array $ctr
604 * Tableau contenant les clés de contrôles. Couples (clé => md5)
605 * @param string $type
606 * Type d'objet
607 * @param int $id
608 * Identifiant de l'objet
609 * @param string $serveur
610 * Nom du connecteur de base de données
611 * @param string $prefix
612 * Préfixe des clés de contrôles : le nom du champ est préfixé de cette valeur
613 * dans le tableau $ctr pour retrouver son md5.
614 * @return null|array
615 * Null si aucun champ ou aucune modification sur les champs
616 * Tableau vide si aucune erreur de contrôle.
617 * Tableau (clé => tableau du conflit). L'index est la colonne en conflit,
618 * la valeur un tableau avec 2 index :
619 * - base : le contenu du champ en base
620 * - post : le contenu posté
621 **/
622 function controler_md5(&$champs, $ctr, $type, $id, $serveur, $prefix = 'ctr_') {
623 $spip_table_objet = table_objet_sql($type);
624 $id_table_objet = id_table_objet($type);
625
626 // Controle des MD5 envoyes
627 // On elimine les donnees non modifiees par le formulaire (mais
628 // potentiellement modifiees entre temps par un autre utilisateur)
629 foreach ($champs as $key => $val) {
630 if (isset($ctr[$prefix . $key]) and $m = $ctr[$prefix . $key]) {
631 if (is_scalar($val) and $m == md5($val)) {
632 unset($champs[$key]);
633 }
634 }
635 }
636 if (!$champs) {
637 return;
638 }
639
640 // On veut savoir si notre modif va avoir un impact
641 // par rapport aux donnees contenues dans la base
642 // (qui peuvent etre differentes de celles ayant servi a calculer le ctr)
643 $s = sql_fetsel(array_keys($champs), $spip_table_objet, "$id_table_objet=$id", $serveur);
644 $intact = true;
645 foreach ($champs as $ch => $val) {
646 $intact &= ($s[$ch] == $val);
647 }
648 if ($intact) {
649 return;
650 }
651
652 // Detection de conflits :
653 // On verifie si notre modif ne provient pas d'un formulaire
654 // genere a partir de donnees modifiees dans l'intervalle ; ici
655 // on compare a ce qui est dans la base, et on bloque en cas
656 // de conflit.
657 $ctrh = $ctrq = $conflits = array();
658 foreach (array_keys($champs) as $key) {
659 if (isset($ctr[$prefix . $key]) and $m = $ctr[$prefix . $key]) {
660 $ctrh[$key] = $m;
661 $ctrq[] = $key;
662 }
663 }
664 if ($ctrq) {
665 $ctrq = sql_fetsel($ctrq, $spip_table_objet, "$id_table_objet=$id", $serveur);
666 foreach ($ctrh as $key => $m) {
667 if ($m != md5($ctrq[$key])
668 and $champs[$key] !== $ctrq[$key]
669 ) {
670 $conflits[$key] = array(
671 'base' => $ctrq[$key],
672 'post' => $champs[$key]
673 );
674 unset($champs[$key]); # stocker quand meme les modifs ?
675 }
676 }
677 }
678
679 return $conflits;
680 }
681
682 /**
683 * Afficher le contenu d'un champ selon sa longueur
684 * soit dans un `textarea`, soit dans un `input`
685 *
686 * @param string $x
687 * texte à afficher
688 * @return string
689 */
690 function display_conflit_champ($x) {
691 if (strstr($x, "\n") or strlen($x) > 80) {
692 return "<textarea style='width:99%; height:10em;'>" . entites_html($x) . "</textarea>\n";
693 } else {
694 return "<input type='text' size='40' style='width:99%' value=\"" . entites_html($x) . "\" />\n";
695 }
696 }
697
698 /**
699 * Signaler une erreur entre 2 saisies d'un champ
700 *
701 * @uses preparer_diff()
702 * @uses propre_diff()
703 * @uses afficher_para_modifies()
704 * @uses afficher_diff()
705 * @uses minipres()
706 *
707 * @param array $conflits
708 * Valeur des champs en conflit
709 * @param string $redirect
710 * @return string
711 */
712 function signaler_conflits_edition($conflits, $redirect = '') {
713 include_spip('inc/minipres');
714 include_spip('inc/revisions');
715 include_spip('afficher_diff/champ');
716 include_spip('inc/suivi_versions');
717 include_spip('inc/diff');
718 $diffs = array();
719 foreach ($conflits as $champ => $a) {
720 // probleme de stockage ou conflit d'edition ?
721 $base = isset($a['save']) ? $a['save'] : $a['base'];
722
723 $diff = new Diff(new DiffTexte);
724 $n = preparer_diff($a['post']);
725 $o = preparer_diff($base);
726 $d = propre_diff(afficher_para_modifies(afficher_diff($diff->comparer($n, $o))));
727
728 $titre = isset($a['save']) ? _L(
729 'Echec lors de l\'enregistrement du champ @champ@',
730 array('champ' => $champ)
731 ) : $champ;
732
733 $diffs[] = "<h2>$titre</h2>\n"
734 . '<h3>' . _T('info_conflit_edition_differences') . "</h3>\n"
735 . "<div style='max-height:8em; overflow: auto; width:99%;'>" . $d . "</div>\n"
736 . '<h4>' . _T('info_conflit_edition_votre_version') . '</h4>'
737 . display_conflit_champ($a['post'])
738 . '<h4>' . _T('info_conflit_edition_version_enregistree') . '</h4>'
739 . display_conflit_champ($base);
740 }
741
742 if ($redirect) {
743 $id = uniqid(rand());
744 $redirect = "<form action='$redirect' method='get'
745 id='$id'
746 style='float:" . $GLOBALS['spip_lang_right'] . "; margin-top:2em;'>\n"
747 . form_hidden($redirect)
748 . "<input type='submit' value='" . _T('icone_retour') . "' />
749 </form>\n";
750
751 // pour les documents, on est probablement en ajax : il faut ajaxer
752 if (_AJAX) {
753 $redirect .= '<script type="text/javascript">'
754 . 'setTimeout(function(){$("#' . $id . '")
755 .ajaxForm({target:$("#' . $id . '").parent()});
756 }, 200);'
757 . "</script>\n";
758 }
759 }
760
761 echo minipres(
762 _T('titre_conflit_edition'),
763 '<style>
764 .diff-para-deplace { background: #e8e8ff; }
765 .diff-para-ajoute { background: #d0ffc0; color: #000; }
766 .diff-para-supprime { background: #ffd0c0; color: #904040; text-decoration: line-through; }
767 .diff-deplace { background: #e8e8ff; }
768 .diff-ajoute { background: #d0ffc0; }
769 .diff-supprime { background: #ffd0c0; color: #802020; text-decoration: line-through; }
770 .diff-para-deplace .diff-ajoute { background: #b8ffb8; border: 1px solid #808080; }
771 .diff-para-deplace .diff-supprime { background: #ffb8b8; border: 1px solid #808080; }
772 .diff-para-deplace .diff-deplace { background: #b8b8ff; border: 1px solid #808080; }
773 </style>'
774 . '<p>' . _T('info_conflit_edition_avis_non_sauvegarde') . '</p>'
775 . '<p>' . _T('texte_conflit_edition_correction') . '</p>'
776 . "<div style='text-align:" . $GLOBALS['spip_lang_left'] . ";'>"
777 . join("\n", $diffs)
778 . "</div>\n"
779
780 . $redirect
781 );
782 }