/**/'; $res['editable'] = true; } else { $res['redirect'] = parametre_url($retour, $id_table_objet, $id); } } } return $res; } /** * Teste les erreurs de validation d'un formulaire d'édition d'objet éditorial * * La fonction teste que : * - il n'y a pas de conflit d'édition sur un ou plusieurs champs (c'est à * dire que personne d'autre n'a modifié le champ entre le moment où on * a saisi et le moment où on a validé le formulaire * - tous les champs obligatoires (listés dans $oblis) sont remplis. * * @api * * @param string $type * Type d'objet * @param int|string $id * Identifiant de l'objet à éditer, 'new' pour un nouvel objet * @param array $oblis * Liste de champs obligatoires : ils doivent avoir un contenu posté. * @return array * Tableau des erreurs **/ function formulaires_editer_objet_verifier($type, $id = 'new', $oblis = array()) { $erreurs = array(); if (intval($id)) { $conflits = controler_contenu($type, $id); if ($conflits and count($conflits)) { foreach ($conflits as $champ => $conflit) { if (!isset($erreurs[$champ])) { $erreurs[$champ] = ''; } $erreurs[$champ] .= _T('alerte_modif_info_concourante') . "
'; } } } foreach ($oblis as $obli) { $value = _request($obli); if (is_null($value) or !(is_array($value) ? count($value) : strlen($value))) { if (!isset($erreurs[$obli])) { $erreurs[$obli] = ''; } $erreurs[$obli] .= _T('info_obligatoire'); } } return $erreurs; } /** * Construit les valeurs de chargement d'un formulaire d'édition d'objet éditorial * * La fonction calcule les valeurs qui seront transmises à l'environnement * du formulaire pour son affichage. Ces valeurs sont les champs de l'objet * éditorial d'une part, mais aussi d'autres calculant la clé d'action, * les pipelines devant faire transiter le contenu HTML du formulaire, * ainsi que différents champs cachés utilisés ensuite dans les traitements. * * Lorsqu'une création d'objet est demandée, ou lorsqu'on demande une traduction * d'un autre, la fonction tente de précharger le contenu de l'objet en * utilisant une fonction inc_precharger_{type}_dist permettant par exemple * de remplir le contenu avec du texte, notamment avec la traduction source. * * @api * * @param string $type * Type d'objet * @param int|string $id * Identifiant de l'objet à éditer, 'new' pour un nouvel objet * @param int $id_parent * Identifiant de l'objet parent * @param int $lier_trad * Identifiant de l'objet servant de source à une nouvelle traduction * @param string $retour * URL de redirection après les traitements * @param string $config_fonc * Nom de fonction appelée au chargement permettant d'ajouter des * valeurs de configurations dans l'environnement du formulaire * @param array $row * Ligne SQL de l'objet édité, si connu. * En absence, les données sont chargées depuis l'objet en base s'il existe * ou depuis l'objet source d'une traduction si c'est un nouvel objet * (et une traduction). * @param string $hidden * Contenu HTML ajouté en même temps que les champs cachés (input hidden) * du formulaire. * @return array * Environnement du formulaire. **/ function formulaires_editer_objet_charger( $type, $id = 'new', $id_parent = 0, $lier_trad = 0, $retour = '', $config_fonc = 'articles_edit_config', $row = array(), $hidden = '' ) { $table_objet = table_objet($type); $table_objet_sql = table_objet_sql($type); $id_table_objet = id_table_objet($type); $new = !is_numeric($id); // Appel direct dans un squelette if (!$row) { if (!$new or $lier_trad) { if ($select = charger_fonction('precharger_' . $type, 'inc', true)) { $row = $select($id, $id_parent, $lier_trad); } else { $row = sql_fetsel('*', $table_objet_sql, $id_table_objet . '=' . intval($id)); } if (!$new) { $md5 = controles_md5($row); } } if (!$row) { $row = array(); $trouver_table = charger_fonction('trouver_table', 'base'); if ($desc = $trouver_table($table_objet)) { foreach ($desc['field'] as $k => $v) { $row[$k] = ''; } } } } // Gaffe: sans ceci, on ecrase systematiquement l'article d'origine // (et donc: pas de lien de traduction) $id = ($new or $lier_trad) ? 'oui' : $row[$id_table_objet]; $row[$id_table_objet] = $id; $contexte = $row; if (strlen($id_parent) && is_numeric($id_parent) && (!isset($contexte['id_parent']) or $new)) { if (!isset($contexte['id_parent'])) { unset($contexte['id_rubrique']); } $contexte['id_parent'] = $id_parent; } elseif (!isset($contexte['id_parent'])) { // id_rubrique dans id_parent si possible if (isset($contexte['id_rubrique'])) { $contexte['id_parent'] = $contexte['id_rubrique']; unset($contexte['id_rubrique']); } else { $contexte['id_parent'] = ''; } if (!$contexte['id_parent'] and $preselectionner_parent_nouvel_objet = charger_fonction('preselectionner_parent_nouvel_objet', 'inc', true) ) { $contexte['id_parent'] = $preselectionner_parent_nouvel_objet($type, $row); } } $config = array(); if ($config_fonc) { $contexte['config'] = $config = $config_fonc($contexte); } $config = $config + array( 'lignes' => 0, 'langue' => '', ); $att_text = " class='textarea' " . " rows='" . ($config['lignes'] + 15) . "' cols='40'"; if (isset($contexte['texte'])) { list($contexte['texte'], $contexte['_texte_trop_long']) = editer_texte_recolle($contexte['texte'], $att_text); } // on veut conserver la langue de l'interface ; // on passe cette donnee sous un autre nom, au cas ou le squelette // voudrait l'exploiter if (isset($contexte['lang'])) { $contexte['langue'] = $contexte['lang']; unset($contexte['lang']); } $contexte['_hidden'] = "\n" . (!$lier_trad ? '' : ("\n" . "\n")) . $hidden . (isset($md5) ? $md5 : ''); // preciser que le formulaire doit passer dans un pipeline $contexte['_pipeline'] = array('editer_contenu_objet', array('type' => $type, 'id' => $id)); // preciser que le formulaire doit etre securise auteur/action // n'est plus utile lorsque l'action accepte l'id en argument direct // on le garde pour compat $contexte['_action'] = array("editer_$type", $id); return $contexte; } /** * Gestion des textes trop longs (limitation brouteurs) * utile pour les textes > 32ko * * @param string $texte * @return array */ function coupe_trop_long($texte) { $aider = charger_fonction('aider', 'inc'); if (strlen($texte) > 28 * 1024) { $texte = str_replace("\r\n", "\n", $texte); $pos = strpos($texte, "\n\n", 28 * 1024); // coupe para > 28 ko if ($pos > 0 and $pos < 32 * 1024) { $debut = substr($texte, 0, $pos) . "\n\n\n"; $suite = substr($texte, $pos + 2); } else { $pos = strpos($texte, ' ', 28 * 1024); // sinon coupe espace if (!($pos > 0 and $pos < 32 * 1024)) { $pos = 28 * 1024; // au pire (pas d'espace trouv'e) $decalage = 0; // si y'a pas d'espace, il ne faut pas perdre le caract`ere } else { $decalage = 1; } $debut = substr($texte, 0, $pos + $decalage); // Il faut conserver l'espace s'il y en a un $suite = substr($texte, $pos + $decalage); } return (array($debut, $suite)); } else { return (array($texte, '')); } } /** * Formater un `$texte` dans `textarea` * * @param string $texte * @param string $att_text * @return array */ function editer_texte_recolle($texte, $att_text) { if ((strlen($texte) < 29 * 1024) or (include_spip('inc/layer') and ($GLOBALS['browser_name'] != 'MSIE')) ) { return array($texte, ''); } include_spip('inc/barre'); $textes_supplement = "
" . _T('info_texte_long') . "\n"; $nombre = 0; while (strlen($texte) > 29 * 1024) { $nombre++; list($texte1, $texte) = coupe_trop_long($texte); $textes_supplement .= '
' . "\n"; } return array($texte, $textes_supplement); } /** * auto-renseigner le titre si il n'existe pas * * @param $champ_titre * @param $champs_contenu * @param int $longueur */ function titre_automatique($champ_titre, $champs_contenu, $longueur = null) { if (!_request($champ_titre)) { $titrer_contenu = charger_fonction('titrer_contenu', 'inc'); if (!is_null($longueur)) { $t = $titrer_contenu($champs_contenu, null, $longueur); } else { $t = $titrer_contenu($champs_contenu); } if ($t) { set_request($champ_titre, $t); } } } /** * Déterminer un titre automatique, * à partir des champs textes de contenu * * Les textes et le titre sont pris dans les champs postés (via `_request()`) * et le titre calculé est de même affecté en tant que champ posté. * * @param array $champs_contenu * Liste des champs contenu textuels * @param array|null $c * tableau qui contient les valeurs des champs de contenu * si `null` on utilise les valeurs du POST * @param int $longueur * Longueur de coupe du texte * @return string */ function inc_titrer_contenu_dist($champs_contenu, $c = null, $longueur = 50) { // trouver un champ texte non vide $t = ''; foreach ($champs_contenu as $champ) { if ($t = _request($champ, $c)) { break; } } if ($t) { include_spip('inc/texte_mini'); $t = couper($t, $longueur, '...'); } return $t; } /** * Calcule des clés de contrôles md5 d'un tableau de données. * * Produit la liste des md5 d'un tableau de données, normalement un * tableau des colonnes/valeurs d'un objet éditorial. * * @param array $data * Couples (colonne => valeur). La valeur est un entier ou un texte. * @param string $prefixe * Préfixe à appliquer sur les noms des clés de contrôles, devant le * nom de la colonne * @param string $format * - html : Retourne les contrôles sous forme de input hidden pour un formulaire * - autre : Retourne le tableau ('$prefixe$colonne => md5) * @return bool|string|array * - false si pas $data n'est pas un tableau * - string (avec format html) : contrôles dans des input hidden * - array sinon couples ('$prefixe$colonne => md5) **/ function controles_md5($data, $prefixe = 'ctr_', $format = 'html') { if (!is_array($data)) { return false; } $ctr = array(); foreach ($data as $key => $val) { $m = md5($val); $k = $prefixe . $key; switch ($format) { case 'html': $ctr[$k] = ""; break; default: $ctr[$k] = $m; break; } } if ($format == 'html') { return "\n\n\n" . join("\n", $ctr) . "\n\n"; } else { return $ctr; } } /** * Contrôle les contenus postés d'un objet en vérifiant qu'il n'y a pas * de conflit d'édition * * Repère les conflits d'édition sur un ou plusieurs champs. C'est à * dire lorsqu'une autre personne a modifié le champ entre le moment où on * a édité notre formulaire et le moment où on a validé le formulaire * * @param string $type * Type d'objet * @param int $id * Identifiant de l'objet * @param array $options * Tableau d'options. Accèpte les index : * - nonvide : Couples (colonne => valeur par défaut). Tous les champs * postés qui sont vides, s'il y en a dans cette option, sont remplacés * par la valeur indiquée * - prefix : Préfixe des clés de contrôles ('ctr_' par défaut). Une clé * de controle tel que 'ctr_titre' contient le md5 du titre au moment * de l'édition. * @param array|bool $c * Tableau de couples (colonne=>valeur) à tester. * Non renseigné, la fonction prend toutes les colonne de l'objet via * _request() * @param string $serveur * Nom du connecteur de base de données * @return bool|null|array * False si aucun champ posté. * Null si aucune modification sur les champs. * Tableau vide si aucun de conflit d'édition. * Tableau (clé => tableau du conflit). L'index est la colonne en conflit, * la valeur un tableau avec 2 index : * - base : le contenu du champ en base * - post : le contenu posté **/ function controler_contenu($type, $id, $options = array(), $c = false, $serveur = '') { include_spip('inc/filtres'); $table_objet = table_objet($type); $spip_table_objet = table_objet_sql($type); $trouver_table = charger_fonction('trouver_table', 'base'); $desc = $trouver_table($table_objet, $serveur); // Appels incomplets (sans $c) if (!is_array($c)) { foreach ($desc['field'] as $champ => $ignore) { if (_request($champ)) { $c[$champ] = _request($champ); } } } // Securite : certaines variables ne sont jamais acceptees ici // car elles ne relevent pas de autoriser(article, modifier) ; // il faut passer par instituer_XX() // TODO: faut-il passer ces variables interdites // dans un fichier de description separe ? unset($c['statut']); unset($c['id_parent']); unset($c['id_rubrique']); unset($c['id_secteur']); // Gerer les champs non vides if (isset($options['nonvide']) and is_array($options['nonvide'])) { foreach ($options['nonvide'] as $champ => $sinon) { if ($c[$champ] === '') { $c[$champ] = $sinon; } } } // N'accepter que les champs qui existent // [TODO] ici aussi on peut valider les contenus en fonction du type $champs = array(); foreach ($desc['field'] as $champ => $ignore) { if (isset($c[$champ])) { $champs[$champ] = $c[$champ]; } } // Nettoyer les valeurs $champs = array_map('corriger_caracteres', $champs); // Envoyer aux plugins $champs = pipeline( 'pre_edition', array( 'args' => array( 'table' => $spip_table_objet, // compatibilite 'table_objet' => $table_objet, 'spip_table_objet' => $spip_table_objet, 'type' => $type, 'id_objet' => $id, 'champs' => isset($options['champs']) ? $options['champs'] : array(), // [doc] c'est quoi ? 'action' => 'controler', 'serveur' => $serveur, ), 'data' => $champs ) ); if (!$champs) { return false; } // Verifier si les mises a jour sont pertinentes, datees, en conflit etc $conflits = controler_md5($champs, $_POST, $type, $id, $serveur, isset($options['prefix']) ? $options['prefix'] : 'ctr_'); return $conflits; } /** * Contrôle la liste des md5 envoyés, supprime les inchangés, * signale les modifiés depuis telle date * * @param array $champs * Couples des champs saisis dans le formulaire (colonne => valeur postée) * @param array $ctr * Tableau contenant les clés de contrôles. Couples (clé => md5) * @param string $type * Type d'objet * @param int $id * Identifiant de l'objet * @param string $serveur * Nom du connecteur de base de données * @param string $prefix * Préfixe des clés de contrôles : le nom du champ est préfixé de cette valeur * dans le tableau $ctr pour retrouver son md5. * @return null|array * Null si aucun champ ou aucune modification sur les champs * Tableau vide si aucune erreur de contrôle. * Tableau (clé => tableau du conflit). L'index est la colonne en conflit, * la valeur un tableau avec 2 index : * - base : le contenu du champ en base * - post : le contenu posté **/ function controler_md5(&$champs, $ctr, $type, $id, $serveur, $prefix = 'ctr_') { $spip_table_objet = table_objet_sql($type); $id_table_objet = id_table_objet($type); // Controle des MD5 envoyes // On elimine les donnees non modifiees par le formulaire (mais // potentiellement modifiees entre temps par un autre utilisateur) foreach ($champs as $key => $val) { if (isset($ctr[$prefix . $key]) and $m = $ctr[$prefix . $key]) { if (is_scalar($val) and $m == md5($val)) { unset($champs[$key]); } } } if (!$champs) { return; } // On veut savoir si notre modif va avoir un impact // par rapport aux donnees contenues dans la base // (qui peuvent etre differentes de celles ayant servi a calculer le ctr) $s = sql_fetsel(array_keys($champs), $spip_table_objet, "$id_table_objet=$id", $serveur); $intact = true; foreach ($champs as $ch => $val) { $intact &= ($s[$ch] == $val); } if ($intact) { return; } // Detection de conflits : // On verifie si notre modif ne provient pas d'un formulaire // genere a partir de donnees modifiees dans l'intervalle ; ici // on compare a ce qui est dans la base, et on bloque en cas // de conflit. $ctrh = $ctrq = $conflits = array(); foreach (array_keys($champs) as $key) { if (isset($ctr[$prefix . $key]) and $m = $ctr[$prefix . $key]) { $ctrh[$key] = $m; $ctrq[] = $key; } } if ($ctrq) { $ctrq = sql_fetsel($ctrq, $spip_table_objet, "$id_table_objet=$id", $serveur); foreach ($ctrh as $key => $m) { if ($m != md5($ctrq[$key]) and $champs[$key] !== $ctrq[$key] ) { $conflits[$key] = array( 'base' => $ctrq[$key], 'post' => $champs[$key] ); unset($champs[$key]); # stocker quand meme les modifs ? } } } return $conflits; } /** * Afficher le contenu d'un champ selon sa longueur * soit dans un `textarea`, soit dans un `input` * * @param string $x * texte à afficher * @return string */ function display_conflit_champ($x) { if (strstr($x, "\n") or strlen($x) > 80) { return "\n"; } else { return "\n"; } } /** * Signaler une erreur entre 2 saisies d'un champ * * @uses preparer_diff() * @uses propre_diff() * @uses afficher_para_modifies() * @uses afficher_diff() * @uses minipres() * * @param array $conflits * Valeur des champs en conflit * @param string $redirect * @return string */ function signaler_conflits_edition($conflits, $redirect = '') { include_spip('inc/minipres'); include_spip('inc/revisions'); include_spip('afficher_diff/champ'); include_spip('inc/suivi_versions'); include_spip('inc/diff'); $diffs = array(); foreach ($conflits as $champ => $a) { // probleme de stockage ou conflit d'edition ? $base = isset($a['save']) ? $a['save'] : $a['base']; $diff = new Diff(new DiffTexte); $n = preparer_diff($a['post']); $o = preparer_diff($base); $d = propre_diff(afficher_para_modifies(afficher_diff($diff->comparer($n, $o)))); $titre = isset($a['save']) ? _L( 'Echec lors de l\'enregistrement du champ @champ@', array('champ' => $champ) ) : $champ; $diffs[] = "

$titre

\n" . '

' . _T('info_conflit_edition_differences') . "

\n" . "
" . $d . "
\n" . '

' . _T('info_conflit_edition_votre_version') . '

' . display_conflit_champ($a['post']) . '

' . _T('info_conflit_edition_version_enregistree') . '

' . display_conflit_champ($base); } if ($redirect) { $id = uniqid(rand()); $redirect = "
\n" . form_hidden($redirect) . "
\n"; // pour les documents, on est probablement en ajax : il faut ajaxer if (_AJAX) { $redirect .= '\n"; } } echo minipres( _T('titre_conflit_edition'), '' . '

' . _T('info_conflit_edition_avis_non_sauvegarde') . '

' . '

' . _T('texte_conflit_edition_correction') . '

' . "
" . join("\n", $diffs) . "
\n" . $redirect ); }