1 <?php
2 /**
3 * Framework for user-facing data validation.
4 *
5 * @package Fieldmanager_Util
6 */
7
8 class Fieldmanager_Util_Validation {
9
10 /**
11 * Instance of this class
12 *
13 * @var Fieldmanager_Util_Validation
14 * @access private
15 */
16 private static $instance;
17
18 /**
19 * @var array
20 * @access private
21 * Array of Fieldmanager fields that require validation
22 */
23 private $fields = array();
24
25 /**
26 * @var array
27 * @access private
28 * Rules for each of the fields to be validated
29 */
30 private $rules = array();
31
32 /**
33 * @var array
34 * @access private
35 * Messages to override the default and display when a field is invalid
36 */
37 private $messages = array();
38
39 /**
40 * @var string
41 * @access private
42 * The form ID that requires validation
43 */
44 private $form_id;
45
46 /**
47 * @var string
48 * @access private
49 * The context this form appears in
50 */
51 private $context;
52
53 /**
54 * @var array
55 * @access private
56 * The allowed validation rules
57 */
58 private $valid_rules = array( 'required', 'remote', 'minlength', 'maxlength', 'rangelength', 'min', 'max', 'range', 'email', 'url', 'date', 'dateISO', 'number', 'digits', 'creditcard', 'equalTo' );
59
60 /**
61 * Singleton helper
62 *
63 * @param array $form_ids
64 * @param string $context
65 * @return object The singleton instance
66 */
67 public static function instance( $form_id, $context ) {
68 // The current form ID and context are used to generate a global variable name to store the instance.
69 // This is necessary so that it persists until all Fieldmanager fields with validation for the current form and context are added.
70 $global_id = $form_id . '_' . $context;
71
72 // Check if this global is set.
73 // If yes, return it.
74 // If not, initialize the singleton instance, set it to the global and return it.
75 if ( isset( $GLOBALS[$global_id] ) ) {
76 return $GLOBALS[$global_id];
77 } else {
78 self::$instance = new Fieldmanager_Util_Validation;
79 self::$instance->setup( $form_id, $context );
80 $GLOBALS[$global_id] = self::$instance;
81 return self::$instance;
82 }
83 }
84
85 /**
86 * Add scripts, initialize variables and add action hooks
87 *
88 * @access private
89 * @param string $form_id
90 * @param string $context
91 * @return Fieldmanager_Util_Validation
92 */
93 private function setup( $form_id, $context ) {
94 // Set class variables
95 $this->form_id = $form_id;
96 $this->context = $context;
97
98 // Add the appropriate action hook to finalize and output validation JS
99 // Also determine where the jQuery validation script needs to be added
100 if ( $context == 'page' ) {
101 // Currently only the page context outputs to the frontend
102 $action = 'wp_footer';
103 $admin = false;
104 } else {
105 // All other contexts are used only on the admin side
106 $action = 'admin_footer';
107 $admin = true;
108 }
109
110 // Hook the action
111 add_action( $action, array( &$this, 'add_validation' ) );
112 }
113
114 /**
115 * Check if a field has validation enabled and if so add it
116 *
117 * @access public
118 * @param Fieldmanager_Field $fm
119 */
120 public function add_field( &$fm ) {
121 // If this field is a Fieldmanager_Group, iterate over the children
122 if ( get_class( $fm ) == "Fieldmanager_Group" ) {
123 foreach ( $fm->children as $child ) {
124 $this->add_field( $child );
125 }
126 }
127
128 // Check if this field has validation enabled. If not, return.
129 if ( empty( $fm->validation_rules ) )
130 return;
131
132 // Determine if the rules are a string or an array and ensure they are valid.
133 // Also aggregate any messages that were set for the rules, ignoring any messages that don't match a rule.
134 $messages = "";
135 if ( ! is_array( $fm->validation_rules ) ) {
136 // If a string, the only acceptable value is "required".
137 if ( ! is_string( $fm->validation_rules ) || $fm->validation_rules != 'required' )
138 $fm->_invalid_definition( sprintf( __( 'The validation rule "%s" does not exist.', 'wordpress-fieldmanager' ), $fm->validation_rules ) );
139
140 // Convert the value to an array since we standardize the Javascript output on this format
141 $fm->validation_rules = array( 'required' => true );
142
143 // In this instance, messages must either be a string or empty. If valid and defined, store this.
144 if ( ! empty( $fm->validation_messages ) && is_string( $fm->validation_messages ) )
145 $messages['required'] = $fm->validation_messages;
146
147 } else {
148 // Verify each rule defined in the array is valid and also check for any messages that were defined for each.
149 foreach ( $fm->validation_rules as $validation_key => $validation_rule ) {
150 if ( ! in_array( $validation_key, $this->valid_rules ) ) {
151 // This is not a rule available in jQuery validation
152 $fm->_invalid_definition( sprintf( __( 'The validation rule "%s" does not exist.', 'wordpress-fieldmanager' ), $validation_key ) );
153 } else {
154 // This rule is valid so check for any messages
155 if ( isset( $fm->validation_messages[$validation_key] ) )
156 $messages[$validation_key] = $fm->validation_messages[$validation_key];
157 }
158 }
159 }
160
161 // If this is the term context and the field is required, modify the original element to have the required property.
162 // This is necessary because it is the only way validation is supported on the term add form.
163 // Other validation methods won't work and will just fail gracefully.
164 if ( $this->context == 'term' && isset( $fm->validation_rules['required'] ) && $fm->validation_rules['required'] )
165 $fm->required = true;
166
167 // If we have reached this point, there were no errors so store the field and the corresponding rules and messages
168 $name = $fm->get_form_name();
169 $this->fields[] = $name;
170 $this->rules[$name] = $fm->validation_rules;
171 $this->messages[$name] = $messages;
172 }
173
174 /**
175 * Output the Javascript required for validation, if any fields require it
176 *
177 * @access public
178 */
179 public function add_validation() {
180 // Iterate through the fields and output the required Javascript
181 $rules = array();
182 $messages = array();
183 foreach ( $this->fields as $field ) {
184 // Add the rule string to an array
185 $rule = $this->value_to_js( $field, $this->rules );
186 if ( ! empty( $rule ) ) {
187 $rules[] = $rule;
188
189 // Add the message to an array, if it exists
190 $message = $this->value_to_js( $field, $this->messages );
191 if ( ! empty( $message ) )
192 $messages[] = $message;
193 }
194 }
195
196 // Create final rule string
197 if ( ! empty( $rules ) ) {
198 $rules_js = $this->array_to_js( $rules, "rules" );
199 $messages_js = $this->array_to_js( $messages, "messages" );
200
201 // Add a comma and newline if messages is not empty
202 if ( ! empty( $messages_js ) ) {
203 $rules_js .= ",\n";
204 }
205
206 // Fields that should always be ignored
207 $ignore[] = ".fm-autocomplete";
208 $ignore[] = "input[type='button']";
209 $ignore[] = ":hidden";
210
211 // Certain fields need to be ignored depending on the context
212 switch ( $this->context ) {
213 case "post":
214 $ignore[] = "#active_post_lock";
215 break;
216 }
217
218 // Add JS for fields to ignore
219 $ignore_js = implode( ", ", $ignore );
220
221 // Add the Fieldmanager validation script and CSS
222 // This is not done via the normal enqueue process since there is no way to know at that point if any fields will require validation
223 // Doing this here avoids loading JS/CSS for validation if not in use
224 echo "<link rel='stylesheet' id='fm-validation-css' href='" . fieldmanager_get_baseurl() . "css/fieldmanager-validation.css' />\n";
225 echo "<script type='text/javascript' src='" . fieldmanager_get_baseurl() . "js/validation/fieldmanager-validation.js?ver=0.3'></script>\n";
226
227 // Add the jQuery validation script
228 echo "<script type='text/javascript' src='" . fieldmanager_get_baseurl() . "js/validation/jquery.validate.min.js'></script>\n";
229
230 // Add the ignore, rules and messages to final validate method with form ID, wrap in script tags and output
231 echo sprintf(
232 "\t<script type='text/javascript'>\n\t\t( function( $ ) {\n\t\t$( document ).ready( function () {\n\t\t\tvar validator = $( '#%s' ).validate( {\n\t\t\t\tinvalidHandler: function( event, validator ) { fm_validation.invalidHandler( event, validator ); },\n\t\t\t\tsubmitHandler: function( form ) { fm_validation.submitHandler( form ); },\n\t\t\t\terrorClass: \"fm-js-error\",\n\t\t\t\tignore: \"%s\",\n%s%s\n\t\t\t} );\n\t\t} );\n\t\t} )( jQuery );\n\t</script>\n",
233 esc_attr( $this->form_id ),
234 $ignore_js,
235 $rules_js,
236 $messages_js
237 );
238 }
239 }
240
241 /**
242 * Converts a single rule or message value into Javascript
243 *
244 * @access private
245 * @param string $field
246 * @param string $data
247 * @return string The Javascript output or an empty string if no data was provided
248 */
249 private function value_to_js( $field, $data ) {
250 // Check the array for the corresponding value. If it doesn't exist, return an empty string.
251 if ( empty( $data[$field] ) )
252 return "";
253
254 // Format the field name
255 $name = $this->quote_field_name( $field );
256
257 // Iterate over the values convert them into a single string
258 $values = array();
259 foreach ( $data[$field] as $k => $v ) {
260 $values[] = sprintf(
261 "\t\t\t\t\t\t%s: %s",
262 esc_js( $k ),
263 $this->format_value( $v )
264 );
265 }
266
267 // Convert the array to a string
268 $value = sprintf(
269 "{\n%s\n\t\t\t\t\t}",
270 implode( ",\n", $values )
271 );
272
273 // Combine the name and value and return it
274 return sprintf(
275 "\t\t\t\t\t%s: %s",
276 $name,
277 $value
278 );
279 }
280
281 /**
282 * Converts an array of values into Javascript
283 *
284 * @access private
285 * @param array $data
286 * @param string $label
287 * @return string The Javascript output or an empty string if no data was provided
288 */
289 private function array_to_js( $data, $label ) {
290 return sprintf(
291 "\t\t\t\t%s: {\n%s\n\t\t\t\t}",
292 esc_js( $label ),
293 implode( ",\n", $data )
294 );
295 }
296
297 /**
298 * Converts a PHP value to the required format for Javascript
299 *
300 * @access private
301 * @param string $value
302 * @return string The formatted value
303 */
304 private function format_value( $value ) {
305 // Determine the data type and return the value formatted appropriately
306 if ( is_bool( $value ) ) {
307 // Convert the value to a string
308 return ( $value ) ? "true" : "false";
309 } else if ( is_numeric( $value ) ) {
310 // Return as-is
311 return $value;
312 } else {
313 // For any other type (should only be a string) escape for JS output
314 return '"' . esc_js( $value ) . '"';
315 }
316 }
317
318 /**
319 * Determine if the field name needs to be quoted for Javascript output
320 *
321 * @access private
322 * @param string $field
323 * @return string The field name with quotes added if necessary
324 */
325 private function quote_field_name( $field ) {
326 // Check if the field name is alphanumeric (underscores and dashes are allowed)
327 if ( ctype_alnum( str_replace( array( '_', '-'), '', $field ) ) )
328 return $field;
329 else
330 return '"' . esc_js( $field ) . '"';
331 }
332 }
333
334 /**
335 * Singleton helper for Fieldmanager_Util_Validation
336 *
337 * @param string $form_id
338 * @param string $context
339 * @return object
340 */
341 function Fieldmanager_Util_Validation( $form_id, $context ) {
342 return Fieldmanager_Util_Validation::instance( $form_id, $context );
343 }
344