1<?php
2/**
3 * Javascript aggregator and builder class
4 *
5 * PHP version 5
6 *
7 * LICENSE
8 *
9 * This source file is subject to BSD 3-Clause License that is bundled
10 * with this package in the file LICENSE and available at the URL
11 * https://raw.githubusercontent.com/pear/HTML_QuickForm2/trunk/docs/LICENSE
12 *
13 * @category  HTML
14 * @package   HTML_QuickForm2
15 * @author    Alexey Borzov <avb@php.net>
16 * @author    Bertrand Mansion <golgote@mamasam.com>
17 * @copyright 2006-2021 Alexey Borzov <avb@php.net>, Bertrand Mansion <golgote@mamasam.com>
18 * @license   https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
19 * @link      https://pear.php.net/package/HTML_QuickForm2
20 */
21
22/**
23 * Exception classes for HTML_QuickForm2
24 */
25require_once 'HTML/QuickForm2/Exception.php';
26
27/**
28 * Javascript aggregator and builder class
29 *
30 * @category HTML
31 * @package  HTML_QuickForm2
32 * @author   Alexey Borzov <avb@php.net>
33 * @author   Bertrand Mansion <golgote@mamasam.com>
34 * @license  https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
35 * @version  Release: @package_version@
36 * @link     https://pear.php.net/package/HTML_QuickForm2
37 */
38class HTML_QuickForm2_JavascriptBuilder
39{
40   /**
41    * Client-side rules
42    * @var array
43    */
44    protected $rules = [];
45
46   /**
47    * Elements' setup code
48    * @var array
49    */
50    protected $scripts = [];
51
52   /**
53    * Whether to generate a validator object for the form if no rules are present
54    *
55    * Needed when the form contains an empty repeat element
56    *
57    * @var array
58    */
59    protected $forceValidator = [];
60
61    /**
62    * Javascript libraries
63    * @var array
64    */
65    protected $libraries = [
66        'base' => ['file' => 'quickform.js']
67    ];
68
69   /**
70    * Default web path to JS library files
71    * @var string
72    */
73    protected $defaultWebPath;
74
75   /**
76    * Default filesystem path to JS library files
77    * @var string
78    */
79    protected $defaultAbsPath;
80
81   /**
82    * Current form ID
83    * @var string
84    */
85    protected $formId = null;
86
87
88   /**
89    * Constructor, sets default web path to JS library files and default filesystem path
90    *
91    * @param string $defaultWebPath default web path to JS library files
92    *                               (to use in <script src="...">)
93    * @param string $defaultAbsPath default filesystem path to JS library files
94    *               (to inline these files into the page), this is set to a package
95    *                subdirectory of PEAR data_dir if not given
96    */
97    public function __construct($defaultWebPath = 'js/', $defaultAbsPath = null)
98    {
99        $this->defaultWebPath = $defaultWebPath;
100
101        if (null === $defaultAbsPath) {
102            $defaultAbsPath = '@data_dir@' . DIRECTORY_SEPARATOR . 'HTML_QuickForm2'
103                              . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR;
104            // package was probably not installed, use relative path
105            if (0 === strpos($defaultAbsPath, '@' . 'data_dir@')) {
106                $defaultAbsPath = realpath(
107                    dirname(dirname(__DIR__))
108                    . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'js'
109                ) . DIRECTORY_SEPARATOR;
110            }
111        }
112        $this->defaultAbsPath = $defaultAbsPath;
113    }
114
115
116   /**
117    * Adds a Javascript library file to the list
118    *
119    * @param string $name     name to reference the library by
120    * @param string $fileName file name, without path
121    * @param string $webPath  path relative to web root to reference in <script src="">,
122    *                         $defaultWebPath will be used if not given
123    * @param string $absPath  filesystem path where the file resides, used when inlining
124    *                         libraries, $defaultAbsPath will be used if not given
125    */
126    public function addLibrary($name, $fileName, $webPath = null, $absPath = null)
127    {
128        $this->libraries[strtolower($name)] = [
129            'file' => $fileName, 'webPath' => $webPath, 'absPath' => $absPath
130        ];
131    }
132
133
134   /**
135    * Returns Javascript libraries
136    *
137    * @param bool $inline        whether to return a list of library file names
138    *                            or contents of files
139    * @param bool $addScriptTags whether to enclose the results in <script> tags
140    *
141    * @return   string|array
142    */
143    public function getLibraries($inline = false, $addScriptTags = true)
144    {
145        $ret = $inline? '': [];
146        foreach ($this->libraries as $name => $library) {
147            if ($inline) {
148                $path = !empty($library['absPath'])? $library['absPath']: $this->defaultAbsPath;
149                if (DIRECTORY_SEPARATOR != substr($path, -1)) {
150                    $path .= DIRECTORY_SEPARATOR;
151                }
152                if (false === ($file = @file_get_contents($path . $library['file']))) {
153                    throw new HTML_QuickForm2_NotFoundException(
154                        "File '{$library['file']}' for JS library '{$name}' not found at '{$path}'"
155                    );
156                }
157                $ret .= ('' == $ret? '': "\n") . $file;
158
159            } else {
160                $path = !empty($library['webPath'])? $library['webPath']: $this->defaultWebPath;
161                if ('/' != substr($path, -1)) {
162                    $path .= '/';
163                }
164                $ret[$name] = $addScriptTags
165                              ? "<script type=\"text/javascript\" src=\"{$path}{$library['file']}\"></script>"
166                              : $path . $library['file'];
167            }
168        }
169        return ($inline && $addScriptTags) ? $this->wrapScript($ret) : $ret;
170    }
171
172
173   /**
174    * Sets ID of the form currently being processed
175    *
176    * All subsequent calls to addRule() and addElementJavascript() will store
177    * the scripts for that form
178    *
179    * @param string $formId
180    */
181    public function setFormId($formId)
182    {
183        $this->formId = $formId;
184        $this->rules[$this->formId]          = [];
185        $this->scripts[$this->formId]        = [];
186        $this->forceValidator[$this->formId] = false;
187    }
188
189
190   /**
191    * Adds the Rule javascript to the list of current form Rules
192    *
193    * @param HTML_QuickForm2_Rule $rule     Rule instance
194    * @param bool                 $triggers Whether rule code should contain
195    *                                       "triggers" for live validation
196    */
197    public function addRule(HTML_QuickForm2_Rule $rule, $triggers = false)
198    {
199        $this->rules[$this->formId][] = $rule->getJavascript($triggers);
200    }
201
202
203   /**
204    * Adds element's setup code to form's Javascript
205    *
206    * @param string $script
207    */
208    public function addElementJavascript($script)
209    {
210        $this->scripts[$this->formId][] = $script;
211    }
212
213
214   /**
215    * Enables generating a validator for the current form even if no rules are present
216    */
217    public function forceValidator()
218    {
219        $this->forceValidator[$this->formId] = true;
220    }
221
222
223   /**
224    * Returns per-form javascript (client-side validation and elements' setup)
225    *
226    * @param string  $formId        form ID, if empty returns code for all forms
227    * @param boolean $addScriptTags whether to enclose code in <script> tags
228    *
229    * @return   string
230    */
231    public function getFormJavascript($formId = null, $addScriptTags = true)
232    {
233        $js  = $this->getValidator($formId, false);
234        $js .= ('' == $js ? '' : "\n") . $this->getSetupCode($formId, false);
235        return $addScriptTags ? $this->wrapScript($js) : $js;
236    }
237
238
239    /**
240     * Returns setup code for form elements
241     *
242     * @param string $formId        form ID, if empty returns code for all forms
243     * @param bool   $addScriptTags whether to enclose code in <script> tags
244     *
245     * @return string
246     */
247    public function getSetupCode($formId = null, $addScriptTags = false)
248    {
249        $js = '';
250        foreach ($this->scripts as $id => $scripts) {
251            if ((null === $formId || $id == $formId) && !empty($scripts)) {
252                $js .= ('' == $js? '': "\n") . implode("\n", $scripts);
253            }
254        }
255        return $addScriptTags ? $this->wrapScript($js) : $js;
256    }
257
258
259    /**
260     * Returns client-side validation code
261     *
262     * @param string $formId        form ID, if empty returns code for all forms
263     * @param bool   $addScriptTags whether to enclose code in <script> tags
264     *
265     * @return string
266     */
267    public function getValidator($formId = null, $addScriptTags = false)
268    {
269        $js = '';
270        foreach ($this->rules as $id => $rules) {
271            if ((null === $formId || $id == $formId)
272                && (!empty($rules) || !empty($this->forceValidator[$id]))
273            ) {
274                $js .= ('' == $js ? '' : "\n")
275                       . "new qf.Validator(document.getElementById('{$id}'), [\n"
276                       . implode(",\n", $rules) . "\n]);";
277            }
278        }
279        return $addScriptTags ? $this->wrapScript($js) : $js;
280    }
281
282    /**
283     * Wraps the given Javascript code in <script> tags
284     *
285     * @param string $js Javascript code
286     *
287     * @return string code wrapped in <script></script> tags,
288     *                empty string if $js is empty
289     */
290    protected function wrapScript($js)
291    {
292        if ('' != $js) {
293            $cr         = HTML_Common2::getOption(HTML_Common2::OPTION_LINEBREAK);
294            $attributes = ' type="text/javascript"';
295            if (null !== ($nonce = HTML_Common2::getOption(HTML_QuickForm2_Node::OPTION_NONCE))) {
296                $attributes .= ' nonce="' . $nonce . '"';
297            }
298            $js = "<script{$attributes}>{$cr}//<![CDATA[{$cr}"
299                  . $js . "{$cr}//]]>{$cr}</script>";
300        }
301        return $js;
302    }
303
304   /**
305    * Encodes a value for use as Javascript literal
306    *
307    * NB: unlike json_encode() we do not enforce UTF-8 charset here
308    *
309    * @param mixed $value
310    *
311    * @return   string  value as Javascript literal
312    */
313    public static function encode($value)
314    {
315        if (is_null($value)) {
316            return 'null';
317
318        } elseif (is_bool($value)) {
319            return $value? 'true': 'false';
320
321        } elseif (is_int($value) || is_float($value)) {
322            return $value;
323
324        } elseif (is_string($value)) {
325            return '"' . strtr($value, [
326                                "\r" => '\r',
327                                "\n" => '\n',
328                                "\t" => '\t',
329                                "'"  => "\\'",
330                                '"'  => '\"',
331                                '\\' => '\\\\'
332                ]) . '"';
333
334        } elseif (is_array($value)) {
335            // associative array, encoding as JS object
336            if (count($value) && array_keys($value) !== range(0, count($value) - 1)) {
337                return '{' . implode(',', array_map(
338                    ['HTML_QuickForm2_JavascriptBuilder', 'encodeNameValue'],
339                    array_keys($value), array_values($value)
340                )) . '}';
341            }
342            return '[' . implode(',', array_map(
343                ['HTML_QuickForm2_JavascriptBuilder', 'encode'],
344                $value
345            )) . ']';
346
347        } elseif (is_object($value)) {
348            $vars = get_object_vars($value);
349            return '{' . implode(',', array_map(
350                ['HTML_QuickForm2_JavascriptBuilder', 'encodeNameValue'],
351                array_keys($vars), array_values($vars)
352            )) . '}';
353
354        } else {
355            throw new HTML_QuickForm2_InvalidArgumentException(
356                'Cannot encode ' . gettype($value) . ' as Javascript value'
357            );
358        }
359    }
360
361
362   /**
363    * Callback for array_map used to generate name-value pairs
364    *
365    * @param mixed $name
366    * @param mixed $value
367    *
368    * @return   string
369    */
370    protected static function encodeNameValue($name, $value)
371    {
372        return self::encode((string)$name) . ':' . self::encode($value);
373    }
374}
375?>
376