source: spip-zone/_plugins_/saisies/trunk/inc/saisies_afficher_si.php @ 113892

Last change on this file since 113892 was 113892, checked in by maieul@…, 8 months ago

Si un afficher_si porte sur un fieldest,
supprimer l'attribut require de tout les input dedans, et pas seulement
ceux de premier niveau, puisque de toute facon les inputs sont forcément
dans un div englobant.
Merci à 1138 pour le signalement.
https://contrib.spip.net/Formidable-le-generateur-de-formulaires#comment500015-500011

File size: 15.9 KB
Line 
1<?php
2
3/**
4 * Gestion de l'affichage conditionnelle des saisies
5 *
6 * @package SPIP\Saisies\Afficher_si
7**/
8
9// Sécurité
10if (!defined('_ECRIRE_INC_VERSION')) {
11        return;
12}
13
14
15/**
16 * Génère, à partir d'un tableau de saisie le code javascript ajouté à la fin de #GENERER_SAISIES
17 * pour produire un affichage conditionnel des saisies ayant une option afficher_si
18 *
19 * @param array  $saisies
20 *                        Tableau de descriptions des saisies
21 * @param string $id_form
22 *                        Identifiant unique pour le formulaire
23 *
24 * @return text
25 *              Code javascript
26 */
27function saisies_generer_js_afficher_si($saisies, $id_form) {
28        $i = 0;
29        $saisies = saisies_lister_par_nom($saisies, true);
30        $code = '';
31        $code .= "$(function(){\n\tchargement=true;\n";
32        $code .= "\tverifier_saisies_".$id_form." = function(form){\n";
33        foreach ($saisies as $saisie) {
34                // on utilise comme selecteur l'identifiant de saisie en priorite s'il est connu
35                // parce que conteneur_class = 'tableau[nom][option]' ne fonctionne evidement pas
36                // lorsque le name est un tableau
37                if (isset($saisie['options']['afficher_si'])) {
38                        ++$i;
39                        // Les [] dans le nom de la saisie sont transformés en _ dans le
40                        // nom de la classe, il faut faire pareil
41                        $nom_underscore = rtrim(
42                                        preg_replace('/[][]\[?/', '_', $saisie['options']['nom']),
43                                        '_'
44                        );
45                        // retrouver la classe css probable
46                        switch ($saisie['saisie']) {
47                                case 'fieldset':
48                                        $class_li = 'fieldset_'.$nom_underscore;
49                                        break;
50                                case 'explication':
51                                        $class_li = 'explication_'.$nom_underscore;
52                                        break;
53                                default:
54                                        // Les [] dans le nom de la saisie sont transformés en _ dans le
55                                        // nom de la classe, il faut faire pareil
56                                        $class_li = 'editer_'.$nom_underscore;
57                        }
58                        $condition = isset($saisie['options']['afficher_si']) ? $saisie['options']['afficher_si'] : '';
59                        // retrouver l'identifiant
60                        $identifiant = '';
61                        if (isset($saisie['identifiant']) and $saisie['identifiant']) {
62                                $identifiant = $saisie['identifiant'];
63                        }
64                        // On gère le cas @plugin:non_plugin@
65                        preg_match_all('#@plugin:(.+)@#U', $condition, $matches);
66                        foreach ($matches[1] as $plug) {
67                                if (defined('_DIR_PLUGIN_'.strtoupper($plug))) {
68                                        $condition = preg_replace('#@plugin:'.$plug.'@#U', 'true', $condition);
69                                } else {
70                                        $condition = preg_replace('#@plugin:'.$plug.'@#U', 'false', $condition);
71                                }
72                        }
73                        $condition = saisies_transformer_condition_afficher_si_config($condition);
74                        // On transforme en une condition valide
75                        preg_match_all('#@(.+)@#U', $condition, $matches);
76                        foreach ($matches[1] as $nom) {
77                                switch ($saisies[$nom]['saisie']) {
78                                        case 'radio':
79                                        case 'oui_non':
80                                        case 'true_false':
81                                                $condition = preg_replace('#@'.preg_quote($nom).'@#U', '$(form).find("[name=\''.$nom.'\']:checked").val()', $condition);
82                                                break;
83                                        case 'case':
84                                                $condition = preg_replace('#@'.preg_quote($nom).'@#U', '($(form).find(".checkbox[name=\''.$nom.'\']").is(":checked") ? $(form).find(".checkbox[name=\''.$nom.'\']").val() : "")', $condition);
85                                                break;
86                                        case 'checkbox':
87                                                /**
88                                                 * Faire fonctionner @checkbox_xx@ == 'valeur' et @checkbox_xx@ != 'valeur'
89                                                 */
90                                                $condition = preg_replace('#@(.+)@\s*(==|(!)=)\s*(\'[^\']*\'|"[^"]*")#U', "@$1@ $3IN $4", $condition );
91                                                /**
92                                                 * Faire fonctionner @checkbox_xx@ IN 'valeur' ou @checkbox_xx@ !IN 'valeur'
93                                                 */
94                                                preg_match_all('#@(.+)@\s*(!IN|IN)\s*[\'"](.*)[\'"]#U', $condition, $matches3);
95                                                foreach ($matches3[3] as $key => $value) {
96                                                        $not = '';
97                                                        if ($matches3[2][$key] == '!IN') {
98                                                                $not = '!';
99                                                        }
100                                                        $values = explode(',', $value);
101                                                        $new_condition = $not.'(';
102                                                        foreach ($values as $key2 => $cond) {
103                                                                if ($key2 > 0) {
104                                                                        $new_condition .= ' || ';
105                                                                }
106                                                                $new_condition .= '($(form).find(".checkbox[name=\''.$nom.'[]\'][value='.$cond.']").is(":checked") ? $(form).find(".checkbox[name=\''.$nom.'[]\'][value='.$cond.']").val() : "") == "'.$cond.'"';
107                                                        }
108                                                        $new_condition .= ')';
109                                                        $condition = str_replace($matches3[0][$key], $new_condition, $condition);
110                                                }
111                                                break;
112                                        default:
113                                                $condition = preg_replace('#@'.preg_quote($nom).'@#U', '$(form).find("[name=\''.$nom.'\']").val()', $condition);
114                                }
115                        }
116                        if ($identifiant) {
117                                $sel = "[data-id='$identifiant']";
118                        } else {
119                                $sel = ".$class_li";
120                        }
121                        $code .= "\tif (".$condition.") {\n"
122                                                         .      "\t\t$(form).find(\"$sel\").show(400);\n";
123                        if (html5_permis()) {
124                        $pour_html_5 =  "$sel.obligatoire > input, "// si le afficher_si porte directement sur le input
125                                                        ."$sel .obligatoire input, "// si le afficher_si porte sur le fieldset, tous les input dedans sont concernés, quelque soit leur profondeur (ce qui inclut notamment les cas complexe, type radio ou date)
126                                                        ."$sel.obligatoire > textarea, "// si le afficher_si porte directement sur le textearea
127                                                        ."$sel .obligatoire > textarea, "// si le afficher_si porte sur le fiedset
128                                                        ."$sel.obligatoire > select, "//si le afficher_si porte directement sur le select
129                                                        ."$sel .obligatoire > select, "//si le afficher_si porte sur le fieldset
130                                                        ."$sel.obligatoire input:radio";//si le afficher_si porte sur un radio
131                        $code .=        "\t\t$(form).find("
132                                                        .'"'."$pour_html_5\")".
133                                                        ".attr(\"required\",true);\n";
134                        }
135                        $code .=        "\t}\n";
136                        $code .= "\telse {\n";
137                        if (html5_permis()) {
138                                $code .= "\t\t$(form).find(\n\t\t\t"
139                                                        .'"'."$pour_html_5\")\n"
140                                                        ."\t\t.attr(".'"required"'.",false);\n";
141                        }
142                        $code .= "\t\tif (chargement==true) {\n"
143                                        ."\t\t\t$(form).find(\"$sel\").hide(400).css".'("display","none")'.";\n"
144                                        ."\t\t}\n"
145                                        ."\t\telse {\n"
146                                        ."\t\t\t$(form).find(\"$sel\").hide(400);\n"
147                                        ."\t\t};\n"
148                                        ."\t}\n";
149                }
150        }
151        $code .= "$(form).trigger('saisies_afficher_si_js_ok');\n";
152        $code .= "};\n";
153        $code .= "\t".'$("#afficher_si_'.$id_form.'").parents("form").each(function(){'."\n\t\t".'verifier_saisies_'.$id_form.'(this);});'."\n";
154        $code .= "\t".'$("#afficher_si_'.$id_form.'").parents("form").change(function(){'."\n\t\t".'verifier_saisies_'.$id_form.'(this);});'."\n";
155        $code .= "\tchargement=false;})\n";
156
157        if (!defined('_SAISIES_AFFICHER_SI_JS_LISIBLE')) {
158                define('_SAISIES_AFFICHER_SI_JS_LISIBLE', false);
159        }
160        if (!_SAISIES_AFFICHER_SI_JS_LISIBLE) {
161                // il suffit de régler cette constante à TRUE pour afficher le js de manière plus lisible (et moins sibyllin)
162                $code = str_replace("\n", '', $code); //concatener
163                $code = str_replace("\t", '', $code); //concatener
164        }
165        return $i > 0 ? $code : '';
166}
167
168/**
169 * Lorsque l'on affiche les saisies (#VOIR_SAISIES), les saisies ayant une option afficher_si
170 * et dont les conditions ne sont pas remplies doivent être retirées du tableau de saisies.
171 *
172 * Cette fonction sert aussi lors de la vérification des saisies avec saisies_verifier().
173 * À ce moment là, les saisies non affichées sont retirées de _request
174 * (on passe leur valeur à NULL).
175 *
176 * @param array      $saisies
177 *                            Tableau de descriptions de saisies
178 * @param array|null $env
179 *                            Tableau d'environnement transmis dans inclure/voir_saisies.html,
180 *                            NULL si on doit rechercher dans _request (pour saisies_verifier()).
181 *
182 * @return array
183 *               Tableau de descriptions de saisies
184 */
185function saisies_verifier_afficher_si($saisies, $env = null) {
186        // eviter une erreur par maladresse d'appel :)
187        if (!is_array($saisies)) {
188                return array();
189        }
190        // Economiser un peu de calcul, notamment pour formidable
191        static $precedent_saisies = array();
192        static $precedent_env = array();
193        if ($precedent_saisies == $saisies and $precedent_env == $env) {
194                return $saisies;
195        }
196        $precedent_saisies = $saisies;
197        $precedent_env = $env;
198        foreach ($saisies as $cle => $saisie) {
199                if (isset($saisie['options']['afficher_si'])) {
200                        $condition = $saisie['options']['afficher_si'];
201                        // Si tentative de code malicieux, on rejete
202                        if (!saisies_verifier_securite_afficher_si($condition)) {
203                                spip_log("Afficher_si malicieuse : $condition", "saisies"._LOG_CRITIQUE);
204                                $condition = '$ok';
205                        }
206                        // Est-ce uniquement au remplissage?
207                        if (isset($saisie['options']['afficher_si_remplissage_uniquement'])
208                                and $saisie['options']['afficher_si_remplissage_uniquement']=='on'){
209                                $remplissage_uniquement = true;
210                        } else {
211                                $remplissage_uniquement = false;
212                        }
213
214                        // On gère le cas @plugin:non_plugin@
215                        preg_match_all('#@plugin:(.+)@#U', $condition, $matches);
216                        foreach ($matches[1] as $plug) {
217                                if (defined('_DIR_PLUGIN_'.strtoupper($plug))) {
218                                        $condition = preg_replace('#@plugin:'.$plug.'@#U', 'true', $condition);
219                                } else {
220                                        $condition = preg_replace('#@plugin:'.$plug.'@#U', 'false', $condition);
221                                }
222                        }
223                        $condition = saisies_transformer_condition_afficher_si_config($condition);
224                        // On transforme en une condition PHP valide
225                        $ok = saisies_evaluer_afficher_si($condition, $env);
226                        if (!$ok) {
227                                if ($remplissage_uniquement == false or is_null($env)) {
228                                        unset($saisies[$cle]);
229                                }
230                                if (is_null($env)) {
231                                        if ($saisie['saisie'] == 'explication') {
232                                                unset($saisies[$cle]);
233                                        } else {
234                                                saisies_set_request_null_recursivement($saisie);
235                                        }
236                                }
237                        }
238                }
239                if (isset($saisies[$cle]['saisies'])) {
240                        // S'il s'agit d'un fieldset ou equivalent, verifier les sous-saisies
241                        $saisies[$cle]['saisies'] = saisies_verifier_afficher_si($saisies[$cle]['saisies'], $env);
242                }
243        }
244        return $saisies;
245}
246
247
248
249/**
250 * Pose un set_request null sur une saisie et toute ses sous-saisies.
251 * Utiliser notamment pour annuler toutes les sous saisies d'un fieldeset
252 * si le fieldset est masquée à cause d'un afficher_si.
253 * @param array $saisie
254**/
255function saisies_set_request_null_recursivement($saisie) {
256        set_request($saisie['options']['nom'], null);
257        if (isset($saisie['saisies'])) {
258                foreach ($saisie['saisies'] as $sous_saisie) {
259                        saisies_set_request_null_recursivement($sous_saisie);
260                }
261        }
262}
263
264/**
265 * Vérifie qu'on ne tente pas de faire executer du code PHP en utilisant afficher_si.
266 * N'importe quoi autorisé entre @@ et "" et ''
267 * Liste de mot clé autorisé en dehors
268 * @param string $condition
269 * @return bool true si usage légitime, false si tentative d'execution de code PHP
270 */
271function saisies_verifier_securite_afficher_si($condition) {
272        $autoriser_hors_guillemets = array("!", "IN", "\(", "\)", "=", "\s", "&&", "\|\|");
273        $autoriser_hors_guillemets = "#(".implode($autoriser_hors_guillemets, "|").")#m";
274
275        $entre_guillemets = "#(?<guillemet>(^\\\)?(\"|'|@))(.*)(\k<guillemet>)#mU"; // trouver tout ce qu'il y entre guillemet, sauf si les guillemets sont échapés
276        $condition = preg_replace($entre_guillemets, "", $condition);//Supprimer tout ce qu'il y a entre guillement
277        $condition = preg_replace($autoriser_hors_guillemets, "", $condition);//Supprimer tout ce qui est autorisé hors guillemets
278        if ($condition) {//S'il restre quelque chose, c'est pas normal
279                return false;
280        }
281        //Sinon c'est que c'est bon
282        return true;
283}
284
285/**
286 * Prend un test conditionnel
287 * cherche dedans les test @config:xxx@
288 * remplace par la valeur de la config
289 * @param string condition;
290 * @return string condition;
291**/
292function saisies_transformer_condition_afficher_si_config($condition) {
293        include_spip("inc/config");
294        preg_match_all('#@config:(.*)@#U', $condition, $matches, PREG_SET_ORDER);
295        foreach ($matches as $plugin) {
296                $arobase = $plugin[0];
297                $config_a_tester = str_replace(":", "/", $plugin[1]);
298                $config = lire_config($config_a_tester);
299                $condition = str_replace($arobase, '"'.$config.'"', $condition);
300        }
301        return $condition;
302}
303
304/**
305 * Prend un test conditionnel,
306 * le sépare en une série de sous-tests de type champ - operateur - valeur
307 * remplace chacun de ces sous-tests par son résultat
308 * renvoie la chaine transformé
309 * @param string $condition
310 * @param array|null $env
311 *   Tableau d'environnement transmis dans inclure/voir_saisies.html,
312 *   NULL si on doit rechercher dans _request (pour saisies_verifier()).
313 * @return string $condition
314**/
315function saisies_transformer_condition_afficher_si($condition, $env = null) {
316        $regexp =
317          "(?<negation>!?)" // négation éventuelle
318                . "(?:@(?<champ>.+?)@)" // @champ_@
319                . "(" // partie operateur + valeur (optionnelle) : debut
320                . "(?:\s*?)" // espaces éventuels après
321                . "(?<operateur>==|!=|IN|!IN)" // opérateur
322                . "(?:\s*?)" // espaces éventuels après
323                . "(?<guillemet>\"|')(?<valeur>.*?)(\k<guillemet>)" // valeur
324                . ")?"; // partie operateur + valeur (optionnelle) : fin
325        $regexp = "#$regexp#";
326        if (preg_match_all($regexp, $condition, $tests, PREG_SET_ORDER)) {
327                foreach ($tests as $test) {
328                        $expression = $test[0];
329                        $champ = $test['champ'];
330                        if (is_null($env)) {
331                                $champ = _request($champ);
332                        } else {
333                                $champ = $env["valeurs"][$champ];
334                        }
335                        $operateur = isset($test['operateur']) ? $test['operateur'] : null;
336                        $valeur = isset($test['valeur']) ? $test['valeur'] : null;
337                        $test_modifie = saisies_tester_condition_afficher_si($champ, $operateur, $valeur) ? 'true' : 'false';
338                        if (isset($test['negation'])) {
339                                $test_modifie = $test['negation'].$test_modifie;
340                        }
341                        $condition = str_replace($expression, $test_modifie, $condition);
342                }
343        }
344        return $condition;
345}
346
347/**
348 * Teste une condition d'afficher_si
349 * @param string|array champ le champ à tester. Cela peut être :
350 *      - un string
351 *      - un tableau
352 *      - un tableau sérializé
353 * @param string $operateur : l'opérateur:
354 *      - IN
355 *      - !IN
356 *      - ==
357 *      - !=
358 *      @param string $valeur la valeur à tester pour un IN. Par exemple "23" ou encore "23", "25"
359 * @return bool false / true selon la condition
360 **/
361function saisies_tester_condition_afficher_si($champ, $operateur=null, $valeur=null) {
362        // Si operateur null => on test juste qu'un champ est cochée / validé
363        if ($operateur === null and $valeur === null) {
364                return isset($champ) and $champ;
365        }
366
367        // Dans tous les cas, enlever les guillemets qui sont au sein de valeur
368        //Si champ est de type string, tenter d'unserializer
369        $tenter_unserialize = @unserialize($champ);
370        if ($tenter_unserialize)  {
371                $champ = $tenter_unserialize;
372        }
373
374        //Et maintenant appeler les sous fonctions qui vont bien
375        if (is_string($champ)) {
376                return saisies_tester_condition_afficher_si_string($champ, $operateur, $valeur);
377        }  elseif (is_array($champ)) {
378                return saisies_tester_condition_afficher_si_array($champ, $operateur, $valeur);
379        } else {
380                return false;
381        }
382}
383
384/**
385 * Teste un condition d'afficher_si lorsqu'il s'agit d'une chaîne
386 * @param string champ le champ à tester.
387 * @param string $operateur : l'opérateur:
388 *      - IN
389 *      - !IN
390 *      - ==
391 *      - !=
392 *      @param string $valeur la valeur à tester pour un IN. Par exemple "23" ou encore "23", "25"
393 * @return bool false / true selon la condition
394**/
395function saisies_tester_condition_afficher_si_string($champ, $operateur, $valeur) {
396        if ($operateur == "==") {
397                return $champ == $valeur;
398        } elseif ($operateur == "!=") {
399                return $champ != $valeur;
400        } else {//Si mauvaise operateur -> on annule
401                return false;
402        }
403}
404
405/**
406 * Teste un condition d'afficher_si lorsqu'il s'agit d'un tableau
407 * @param array champ le champ à tester.
408 * @param string $operateur : l'opérateur:
409 *      - IN
410 *      - !IN
411 *      - ==
412 *      - !=
413 * @param string $valeur la valeur à tester pour un IN. Par exemple "23" ou encore "23", "25"
414 * @return bool false / true selon la condition
415**/
416function saisies_tester_condition_afficher_si_array($champ, $operateur, $valeur) {
417        $valeur = explode(',', $valeur);
418        $intersection = array_intersect($champ, $valeur);
419        if ($operateur == "==" or $operateur == "IN") {
420                return count($intersection) > 0;
421        } else {
422                return count($intersection) == 0;
423        }
424        return false;
425}
426
427/**
428 * Evalue un afficher_si
429 * @param string $condition (déjà checkée en terme de sécurité)
430 * @param array|null $env
431 *   Tableau d'environnement transmis dans inclure/voir_saisies.html,
432 *   NULL si on doit rechercher dans _request (pour saisies_verifier()).
433 * @return bool le résultat du test
434**/
435function saisies_evaluer_afficher_si($condition, $env = null) {
436        $condition = saisies_transformer_condition_afficher_si($condition, $env);
437        eval('$ok = '.$condition.';');
438        return $ok;
439}
Note: See TracBrowser for help on using the repository browser.