3 /***************************************************************************\
4 * SPIP, Systeme de publication pour l'internet *
6 * Copyright (c) 2001-2019 *
7 * Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James *
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 \***************************************************************************/
14 * Fonctions d'aide à l'édition d'objets éditoriaux.
16 * @package SPIP\Core\Edition
19 if (!defined('_ECRIRE_INC_VERSION')) {
22 include_spip('base/abstract_sql');
25 * Effectue les traitements d'un formulaire d'édition d'objet éditorial
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())
31 * Si une traduction était demandée, crée le lien avec l'objet qui est
35 * @see action_editer_objet_dist()
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
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)
59 * Retour des traitements.
61 function formulaires_editer_objet_traiter(
67 $config_fonc = 'articles_edit_config',
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);
78 $action_editer = charger_fonction('editer_objet', 'action');
79 list($id, $err) = $action_editer($id, $type);
81 $id_table_objet = id_table_objet($type);
82 $res[$id_table_objet] = $id;
84 $res['message_erreur'] = ($err ?
$err : _T('erreur'));
86 // Un lien de trad a prendre en compte
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);
95 $res['message_erreur'] = $err;
100 $res['message_ok'] = _T('info_modification_enregistree');
102 if (strncmp($retour, 'javascript:', 11) == 0) {
103 $res['message_ok'] .= '<script type="text/javascript">/*<![CDATA[*/' . substr($retour, 11) . '/*]]>*/</script>';
104 $res['editable'] = true;
106 $res['redirect'] = parametre_url($retour, $id_table_objet, $id);
115 * Teste les erreurs de validation d'un formulaire d'édition d'objet éditorial
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.
125 * @param string $type
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é.
132 * Tableau des erreurs
134 function formulaires_editer_objet_verifier($type, $id = 'new', $oblis = array()) {
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] = '';
143 $erreurs[$champ] .= _T('alerte_modif_info_concourante') . "<br /><textarea readonly='readonly' class='forml'>" . entites_html($conflit['base']) . '</textarea>';
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] = '';
153 $erreurs[$obli] .= _T('info_obligatoire');
161 * Construit les valeurs de chargement d'un formulaire d'édition d'objet éditorial
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.
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.
176 * @param string $type
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
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)
198 * Environnement du formulaire.
200 function formulaires_editer_objet_charger(
206 $config_fonc = 'articles_edit_config',
211 $table_objet = table_objet($type);
212 $table_objet_sql = table_objet_sql($type);
213 $id_table_objet = id_table_objet($type);
215 // on accepte pas une fonction de config inconnue si elle vient d'un modele
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)) {
225 $new = !is_numeric($id);
226 // Appel direct dans un squelette
228 if (!$new or $lier_trad) {
229 if ($select = charger_fonction('precharger_' . $type, 'inc', true)) {
230 $row = $select($id, $id_parent, $lier_trad);
232 $row = sql_fetsel('*', $table_objet_sql, $id_table_objet . '=' . intval($id));
235 $md5 = controles_md5($row);
240 $trouver_table = charger_fonction('trouver_table', 'base');
241 if ($desc = $trouver_table($table_objet)) {
242 foreach ($desc['field'] as $k => $v) {
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)
253 : $row[$id_table_objet];
254 $row[$id_table_objet] = $id;
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']);
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']);
268 $contexte['id_parent'] = '';
270 if (!$contexte['id_parent']
271 and $preselectionner_parent_nouvel_objet = charger_fonction('preselectionner_parent_nouvel_objet', 'inc', true)
273 $contexte['id_parent'] = $preselectionner_parent_nouvel_objet($type, $row);
279 $contexte['config'] = $config = $config_fonc($contexte);
281 $config = $config +
array(
286 $att_text = " class='textarea' "
288 . ($config['lignes'] +
15)
290 if (isset($contexte['texte'])) {
291 list($contexte['texte'], $contexte['_texte_trop_long']) = editer_texte_recolle($contexte['texte'], $att_text);
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']);
302 $contexte['_hidden'] = "<input type='hidden' name='editer_$type' value='oui' />\n" .
304 ("\n<input type='hidden' name='lier_trad' value='" .
307 "\n<input type='hidden' name='changer_lang' value='" .
311 . (isset($md5) ?
$md5 : '');
313 // preciser que le formulaire doit passer dans un pipeline
314 $contexte['_pipeline'] = array('editer_contenu_objet', array('type' => $type, 'id' => $id));
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);
325 * Gestion des textes trop longs (limitation brouteurs)
326 * utile pour les textes > 32ko
328 * @param string $texte
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);
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
347 $debut = substr($texte, 0, $pos +
$decalage); // Il faut conserver l'espace s'il y en a un
348 $suite = substr($texte, $pos +
$decalage);
351 return (array($debut, $suite));
353 return (array($texte, ''));
358 * Formater un `$texte` dans `textarea`
360 * @param string $texte
361 * @param string $att_text
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'))
368 return array($texte, '');
371 include_spip('inc/barre');
372 $textes_supplement = "<br /><span style='color: red'>" . _T('info_texte_long') . "</span>\n";
375 while (strlen($texte) > 29 * 1024) {
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";
382 return array($texte, $textes_supplement);
386 * auto-renseigner le titre si il n'existe pas
388 * @param $champ_titre
389 * @param $champs_contenu
390 * @param int $longueur
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);
398 $t = $titrer_contenu($champs_contenu);
401 set_request($champ_titre, $t);
407 * Déterminer un titre automatique,
408 * à partir des champs textes de contenu
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é.
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
422 function inc_titrer_contenu_dist($champs_contenu, $c = null, $longueur = 50) {
423 // trouver un champ texte non vide
425 foreach ($champs_contenu as $champ) {
426 if ($t = _request($champ, $c)) {
432 include_spip('inc/texte_mini');
433 $t = couper($t, $longueur, '...');
440 * Calcule des clés de contrôles md5 d'un tableau de données.
442 * Produit la liste des md5 d'un tableau de données, normalement un
443 * tableau des colonnes/valeurs d'un objet éditorial.
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
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)
458 function controles_md5($data, $prefixe = 'ctr_', $format = 'html') {
459 if (!is_array($data)) {
464 foreach ($data as $key => $val) {
466 $k = $prefixe . $key;
470 $ctr[$k] = "<input type='hidden' value='$m' name='$k' />";
478 if ($format == 'html') {
479 return "\n\n<!-- controles md5 -->\n" . join("\n", $ctr) . "\n\n";
486 * Contrôle les contenus postés d'un objet en vérifiant qu'il n'y a pas
487 * de conflit d'édition
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
493 * @param string $type
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
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
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é
520 function controler_contenu($type, $id, $options = array(), $c = false, $serveur = '') {
521 include_spip('inc/filtres');
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);
528 // Appels incomplets (sans $c)
530 foreach ($desc['field'] as $champ => $ignore) {
531 if (_request($champ)) {
532 $c[$champ] = _request($champ);
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 ?
543 unset($c['id_parent']);
544 unset($c['id_rubrique']);
545 unset($c['id_secteur']);
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] === '') {
556 // N'accepter que les champs qui existent
557 // [TODO] ici aussi on peut valider les contenus en fonction du type
559 foreach ($desc['field'] as $champ => $ignore) {
560 if (isset($c[$champ])) {
561 $champs[$champ] = $c[$champ];
565 // Nettoyer les valeurs
566 $champs = array_map('corriger_caracteres', $champs);
568 // Envoyer aux plugins
573 'table' => $spip_table_objet, // compatibilite
574 'table_objet' => $table_objet,
575 'spip_table_objet' => $spip_table_objet,
578 'champs' => isset($options['champs']) ?
$options['champs'] : array(), // [doc] c'est quoi ?
579 'action' => 'controler',
580 'serveur' => $serveur,
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_');
598 * Contrôle la liste des md5 envoyés, supprime les inchangés,
599 * signale les modifiés depuis telle date
601 * @param array $champs
602 * Couples des champs saisis dans le formulaire (colonne => valeur postée)
604 * Tableau contenant les clés de contrôles. Couples (clé => md5)
605 * @param string $type
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.
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é
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);
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]);
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);
645 foreach ($champs as $ch => $val) {
646 $intact &= ($s[$ch] == $val);
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
657 $ctrh = $ctrq = $conflits = array();
658 foreach (array_keys($champs) as $key) {
659 if (isset($ctr[$prefix . $key]) and $m = $ctr[$prefix . $key]) {
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]
670 $conflits[$key] = array(
671 'base' => $ctrq[$key],
672 'post' => $champs[$key]
674 unset($champs[$key]); # stocker quand meme les modifs ?
683 * Afficher le contenu d'un champ selon sa longueur
684 * soit dans un `textarea`, soit dans un `input`
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";
694 return "<input type='text' size='40' style='width:99%' value=\"" . entites_html($x) . "\" />\n";
699 * Signaler une erreur entre 2 saisies d'un champ
701 * @uses preparer_diff()
702 * @uses propre_diff()
703 * @uses afficher_para_modifies()
704 * @uses afficher_diff()
707 * @param array $conflits
708 * Valeur des champs en conflit
709 * @param string $redirect
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');
719 foreach ($conflits as $champ => $a) {
720 // probleme de stockage ou conflit d'edition ?
721 $base = isset($a['save']) ?
$a['save'] : $a['base'];
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))));
728 $titre = isset($a['save']) ?
_L(
729 'Echec lors de l\'enregistrement du champ @champ@',
730 array('champ' => $champ)
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);
743 $id = uniqid(rand());
744 $redirect = "<form action='$redirect' method='get'
746 style='float:" . $GLOBALS['spip_lang_right'] . "; margin-top:2em;'>\n"
747 . form_hidden($redirect)
748 . "<input type='submit' value='" . _T('icone_retour') . "' />
751 // pour les documents, on est probablement en ajax : il faut ajaxer
753 $redirect .= '<script type="text/javascript">'
754 . 'setTimeout(function(){$("#' . $id . '")
755 .ajaxForm({target:$("#' . $id . '").parent()});
762 _T('titre_conflit_edition'),
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; }
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'] . ";'>"