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