Server : Apache System : Linux ks5.tuic.fr 6.1.0-18-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.76-1 (2024-02-01) x86_64 User : pragmatice ( 1003) PHP Version : 8.2.24 Disable Function : NONE Directory : /home/pragmatice/public_html/lesite/ecrire/inc/ |
<?php /***************************************************************************\ * SPIP, Système de publication pour l'internet * * * * Copyright © avec tendresse depuis 2001 * * Arnaud Martin, Antoine Pitrou, Philippe Rivière, Emmanuel Saint-James * * * * Ce programme est un logiciel libre distribué sous licence GNU/GPL. * * Pour plus de détails voir le fichier COPYING.txt ou l'aide en ligne. * \***************************************************************************/ /** * Fonctions d'aide à l'édition d'objets éditoriaux. * * @package SPIP\Core\Edition **/ if (!defined('_ECRIRE_INC_VERSION')) { return; } include_spip('base/abstract_sql'); /** * Effectue les traitements d'un formulaire d'édition d'objet éditorial * * Exécute une action d'édition spécifique au type d'objet s'il elle existe * (fonction action_editer_$type), sinon exécute l'action générique * d'édition d'objet (action_editer_objet_dist()) * * Si une traduction était demandée, crée le lien avec l'objet qui est * traduit. * * @api * @see action_editer_objet_dist() * * @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 * Retour des traitements. **/ function formulaires_editer_objet_traiter( $type, $id = 'new', $id_parent = 0, $lier_trad = 0, $retour = '', $config_fonc = 'articles_edit_config', $row = [], $hidden = '' ) { $res = []; // eviter la redirection forcee par l'action... set_request('redirect'); if ($action_editer = charger_fonction("editer_$type", 'action', true)) { [$id, $err] = $action_editer($id); } else { $action_editer = charger_fonction('editer_objet', 'action'); [$id, $err] = $action_editer($id, $type); } $id_table_objet = id_table_objet($type); $res[$id_table_objet] = $id; if ($err or !$id) { $res['message_erreur'] = ($err ?: _T('erreur')); } else { // Un lien de trad a prendre en compte if ($lier_trad) { // referencer la traduction $referencer_traduction = charger_fonction('referencer_traduction', 'action'); $referencer_traduction($type, $id, $lier_trad); // actions de recopie de champs / liens sur le nouvel objet créé $completer_traduction = charger_fonction('completer_traduction', 'inc'); $err = $completer_traduction($type, $id, $lier_trad); if ($err) { $res['message_erreur'] = $err; return $res; } } $res['message_ok'] = _T('info_modification_enregistree'); if ($retour) { if (strncmp($retour, 'javascript:', 11) == 0) { $res['message_ok'] .= '<script type="text/javascript">/*<![CDATA[*/' . substr($retour, 11) . '/*]]>*/</script>'; $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 = []) { $erreurs = []; if (intval($id)) { $conflits = controler_contenu($type, $id); if ($conflits and is_countable($conflits) ? count($conflits) : 0) { foreach ($conflits as $champ => $conflit) { if (!isset($erreurs[$champ])) { $erreurs[$champ] = ''; } $erreurs[$champ] .= _T('alerte_modif_info_concourante') . "<br /><textarea readonly='readonly' class='forml'>" . entites_html($conflit['base']) . '</textarea>'; } } } 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|null $id_parent * Identifiant de l'objet parent * Si null, le calcule d’après le contexte. * @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 = [], $hidden = '' ) { $table_objet = table_objet($type); $table_objet_sql = table_objet_sql($type); $id_table_objet = id_table_objet($type); if (!is_array($row)) { $row = []; } // on accepte pas une fonction de config inconnue si elle vient d'un modele if ( $config_fonc and !in_array($config_fonc, ['articles_edit_config', 'rubriques_edit_config', 'auteurs_edit_config']) and $config_fonc !== $table_objet . '_edit_config' ) { if ( $args = test_formulaire_inclus_par_modele() and in_array($config_fonc, $args) ) { $config_fonc = ''; } } $new = !is_numeric($id); $lang_default = ''; // 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); // si on a une fonction precharger, elle pu faire un reglage de langue $lang_default = (!empty($row['lang']) ? $row['lang'] : null); } else { $row = sql_fetsel('*', $table_objet_sql, $id_table_objet . '=' . intval($id)); } if (!$new) { $md5 = controles_md5($row ?: []); } } if (!$row) { $row = []; $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 (is_numeric($id_parent) && strlen($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 = []; if ($config_fonc) { $contexte['config'] = $config = $config_fonc($contexte); if (!$lang_default) { include_spip('inc/session'); $lang_default = $config['langue'] ?? session_get('lang') ; } } $config = $config + [ 'lignes' => 0, 'langue' => '', ]; $att_text = " class='textarea' " . " rows='" . ($config['lignes'] + 15) . "' cols='40'"; if (isset($contexte['texte'])) { [$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'] = "<input type='hidden' name='editer_$type' value='oui' />\n" . (!$lier_trad ? '' : ("\n<input type='hidden' name='lier_trad' value='" . $lier_trad . "' />" . "\n<input type='hidden' name='changer_lang' value='" . $lang_default . "' />")) . $hidden . ($md5 ?? ''); // preciser que le formulaire doit passer dans un pipeline $contexte['_pipeline'] = ['editer_contenu_objet', ['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'] = ["editer_$type", $id]; // et in fine placer l'autorisation include_spip('inc/autoriser'); if (intval($id)) { if (!autoriser('modifier', $type, intval($id))) { $contexte['editable'] = ''; } } else { if (!autoriser('creer', $type, 0, null, ['id_parent' => $id_parent])) { $contexte['editable'] = ''; } } 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<!--SPIP-->\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 ([$debut, $suite]); } else { return ([$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 [$texte, '']; } include_spip('inc/barre'); $textes_supplement = "<br /><span style='color: red'>" . _T('info_texte_long') . "</span>\n"; $nombre = 0; while (strlen($texte) > 29 * 1024) { $nombre++; [$texte1, $texte] = coupe_trop_long($texte); $textes_supplement .= '<br />' . "<textarea id='texte$nombre' name='texte_plus[$nombre]'$att_text>$texte1</textarea>\n"; } return [$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 string|array * - string (avec format html) : contrôles dans des input hidden * - array sinon couples ('$prefixe$colonne => md5) **/ function controles_md5(array $data, string $prefixe = 'ctr_', string $format = 'html') { $ctr = []; foreach ($data as $key => $val) { $m = md5($val ?? ''); $k = $prefixe . $key; switch ($format) { case 'html': $ctr[$k] = "<input type='hidden' value='$m' name='$k' />"; break; default: $ctr[$k] = $m; break; } } if ($format === 'html') { return "\n\n<!-- controles md5 -->\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 = [], $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)) { $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 = []; 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', [ 'args' => [ 'table' => $spip_table_objet, // compatibilite 'table_objet' => $table_objet, 'spip_table_objet' => $spip_table_objet, 'type' => $type, 'id_objet' => $id, 'champs' => $options['champs'] ?? [], // [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, $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 = []; 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] = [ '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 "<textarea style='width:99%; height:10em;'>" . entites_html($x) . "</textarea>\n"; } else { return "<input type='text' size='40' style='width:99%' value=\"" . entites_html($x) . "\" />\n"; } }