1<?php
2/**
3 * Base class for HTML_QuickForm2 rules
4 *
5 * PHP version 5
6 *
7 * LICENSE:
8 *
9 * Copyright (c) 2006-2010, Alexey Borzov <avb@php.net>,
10 *                          Bertrand Mansion <golgote@mamasam.com>
11 * All rights reserved.
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions
15 * are met:
16 *
17 *    * Redistributions of source code must retain the above copyright
18 *      notice, this list of conditions and the following disclaimer.
19 *    * Redistributions in binary form must reproduce the above copyright
20 *      notice, this list of conditions and the following disclaimer in the
21 *      documentation and/or other materials provided with the distribution.
22 *    * The names of the authors may not be used to endorse or promote products
23 *      derived from this software without specific prior written permission.
24 *
25 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
26 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
27 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
28 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
29 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
31 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
33 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36 *
37 * @category   HTML
38 * @package    HTML_QuickForm2
39 * @author     Alexey Borzov <avb@php.net>
40 * @author     Bertrand Mansion <golgote@mamasam.com>
41 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
42 * @version    SVN: $Id: Rule.php 299706 2010-05-24 18:32:37Z avb $
43 * @link       http://pear.php.net/package/HTML_QuickForm2
44 */
45
46/**
47 * Abstract base class for HTML_QuickForm2 rules
48 *
49 * This class provides methods that allow chaining several rules together.
50 * Its validate() method executes the whole rule chain starting from this rule.
51 *
52 * @category   HTML
53 * @package    HTML_QuickForm2
54 * @author     Alexey Borzov <avb@php.net>
55 * @author     Bertrand Mansion <golgote@mamasam.com>
56 * @version    Release: @package_version@
57 */
58abstract class HTML_QuickForm2_Rule
59{
60   /**
61    * Constant showing that validation should be run server-side
62    * @see  HTML_QuickForm2_Node::addRule()
63    */
64    const RUNAT_SERVER = 1;
65
66   /**
67    * Constant showing that validation should be run client-side
68    * @see  HTML_QuickForm2_Node::addRule()
69    */
70    const RUNAT_CLIENT = 2;
71
72   /**
73    * An element whose value will be validated by this rule
74    * @var  HTML_QuickForm2_Node
75    */
76    protected $owner;
77
78   /**
79    * An error message to display if validation fails
80    * @var  string
81    */
82    protected $message;
83
84   /**
85    * Configuration data for the rule
86    * @var  mixed
87    */
88    protected $config;
89
90   /**
91    * Rules chained to this via "and" and "or" operators
92    *
93    * The contents can be described as "disjunctive normal form", where an outer
94    * array represents a disjunction of conjunctive clauses represented by inner
95    * arrays.
96    *
97    * @var  array
98    */
99    protected $chainedRules = array(array());
100
101
102   /**
103    * Class constructor
104    *
105    * @param    HTML_QuickForm2_Node    Element to validate
106    * @param    string                  Error message to display if validation fails
107    * @param    mixed                   Configuration data for the rule
108    */
109    public function __construct(HTML_QuickForm2_Node $owner, $message = '', $config = null)
110    {
111        $this->setOwner($owner);
112        $this->setMessage($message);
113        $this->setConfig($config);
114    }
115
116   /**
117    * Merges local configuration with that provided for registerRule()
118    *
119    * Default behaviour is for global config to override local one, different
120    * Rules may implement more complex merging behaviours.
121    *
122    * @param    mixed   Local configuration
123    * @param    mixed   Global configuration, usually provided to {@link HTML_QuickForm2_Factory::registerRule()}
124    * @return   mixed   Merged configuration
125    */
126    public static function mergeConfig($localConfig, $globalConfig)
127    {
128        return is_null($globalConfig)? $localConfig: $globalConfig;
129    }
130
131   /**
132    * Sets configuration data for the rule
133    *
134    * @param    mixed   Rule configuration data (specific for a Rule)
135    * @return   HTML_QuickForm2_Rule
136    * @throws   HTML_QuickForm2_InvalidArgumentException    in case of invalid
137    *               configuration data
138    */
139    public function setConfig($config)
140    {
141        $this->config = $config;
142        return $this;
143    }
144
145   /**
146    * Returns the rule's configuration data
147    *
148    * @return   mixed   Configuration data (specific for a Rule)
149    */
150    public function getConfig()
151    {
152        return $this->config;
153    }
154
155   /**
156    * Sets the error message output by the rule
157    *
158    * @param    string                  Error message to display if validation fails
159    * @return   HTML_QuickForm2_Rule
160    */
161    public function setMessage($message)
162    {
163        $this->message = (string)$message;
164        return $this;
165    }
166
167   /**
168    * Returns the error message output by the rule
169    *
170    * @return   string  Error message
171    */
172    public function getMessage()
173    {
174        return $this->message;
175    }
176
177   /**
178    * Sets the element that will be validated by this rule
179    *
180    * @param    HTML_QuickForm2_Node    Element to validate
181    */
182    public function setOwner(HTML_QuickForm2_Node $owner)
183    {
184        if (null !== $this->owner) {
185            $this->owner->removeRule($this);
186        }
187        $this->owner = $owner;
188    }
189
190   /**
191    * Adds a rule to the chain with an "and" operator
192    *
193    * Evaluation is short-circuited, next rule will not be evaluated if the
194    * previous one returns false. The method is named this way because "and" is
195    * a reserved word in PHP.
196    *
197    * @param    HTML_QuickForm2_Rule
198    * @return   HTML_QuickForm2_Rule    first rule in the chain (i.e. $this)
199    * @throws   HTML_QuickForm2_InvalidArgumentException    when trying to add
200    *           a "required" rule to the chain
201    */
202    public function and_(HTML_QuickForm2_Rule $next)
203    {
204        if ($next instanceof HTML_QuickForm2_Rule_Required) {
205            throw new HTML_QuickForm2_InvalidArgumentException(
206                'and_(): Cannot add a "required" rule'
207            );
208        }
209        $this->chainedRules[count($this->chainedRules) - 1][] = $next;
210        return $this;
211    }
212
213   /**
214    * Adds a rule to the chain with an "or" operator
215    *
216    * Evaluation is short-circuited, next rule will not be evaluated if the
217    * previous one returns true. The method is named this way because "or" is
218    * a reserved word in PHP.
219    *
220    * @param    HTML_QuickForm2_Rule
221    * @return   HTML_QuickForm2_Rule    first rule in the chain (i.e. $this)
222    * @throws   HTML_QuickForm2_InvalidArgumentException    when trying to add
223    *           a "required" rule to the chain
224    */
225    public function or_(HTML_QuickForm2_Rule $next)
226    {
227        if ($next instanceof HTML_QuickForm2_Rule_Required) {
228            throw new HTML_QuickForm2_InvalidArgumentException(
229                'or_(): Cannot add a "required" rule'
230            );
231        }
232        $this->chainedRules[] = array($next);
233        return $this;
234    }
235
236   /**
237    * Performs validation
238    *
239    * The whole rule chain is executed. Note that the side effect of this
240    * method is setting the error message on element if validation fails
241    *
242    * @return   boolean     Whether the element is valid
243    */
244    public function validate()
245    {
246        $globalValid = false;
247        $localValid  = $this->validateOwner();
248        foreach ($this->chainedRules as $item) {
249            foreach ($item as $multiplier) {
250                if (!($localValid = $localValid && $multiplier->validate())) {
251                    break;
252                }
253            }
254            if ($globalValid = $globalValid || $localValid) {
255                break;
256            }
257            $localValid = true;
258        }
259        $globalValid or $this->setOwnerError();
260        return $globalValid;
261    }
262
263   /**
264    * Validates the owner element
265    *
266    * @return   bool    Whether owner element is valid according to the rule
267    */
268    abstract protected function validateOwner();
269
270   /**
271    * Sets the error message on the owner element
272    */
273    protected function setOwnerError()
274    {
275        if (strlen($this->getMessage()) && !$this->owner->getError()) {
276            $this->owner->setError($this->getMessage());
277        }
278    }
279
280   /**
281    * Returns the client-side validation callback
282    *
283    * This essentially builds a Javascript version of validateOwner() method,
284    * with element ID and Rule configuration hardcoded.
285    *
286    * @return   string    Javascript function to validate the element's value
287    * @throws   HTML_QuickForm2_Exception   if Rule can only be run server-side
288    */
289    protected function getJavascriptCallback()
290    {
291        throw new HTML_QuickForm2_Exception(
292            get_class($this) . ' does not implement javascript validation'
293        );
294    }
295
296   /**
297    * Returns the client-side representation of the Rule
298    *
299    * The Javascript object returned contains the following fields:
300    *  - callback: {@see getJavascriptCallback()}
301    *  - elementId: element ID to set error for if validation fails
302    *  - errorMessage: error message to set if validation fails
303    *  - chained: chained rules, array of arrays like in $chainedRules property
304    *
305    * @return   string
306    * @throws   HTML_QuickForm2_Exception   if Rule or its chained Rules can only
307    *                                       be run server-side
308    */
309    public function getJavascript()
310    {
311        $js = "{\n\tcallback: " . $this->getJavascriptCallback() . ",\n" .
312              "\telementId: '" . $this->owner->getId() . "',\n" .
313              "\terrorMessage: '" . strtr($this->getMessage(), array(
314                    "\r"    => '\r',
315                    "\n"    => '\n',
316                    "\t"    => '\t',
317                    "'"     => "\\'",
318                    '"'     => '\"',
319                    '\\'    => '\\\\'
320              )) . "',\n\tchained: [";
321        $chained = array();
322        foreach ($this->chainedRules as $item) {
323            $multipliers = array();
324            foreach ($item as $multiplier) {
325                $multipliers[] = $multiplier->getJavascript();
326            }
327            $chained[] = '[' . implode(",\n", $multipliers) . ']';
328        }
329        $js .= implode(",\n", $chained) . "]\n}";
330        return $js;
331    }
332}
333?>
334