MediaWiki fundraising/REL1_35
DataValidator.php
Go to the documentation of this file.
1<?php
2
3use SmashPig\Core\ValidationError;
4
17
26 public static function getErrorToken( $field ) {
27 switch ( $field ) {
28 case 'employer':
29 case 'email':
30 case 'amount':
31 case 'currency':
32 case 'fiscal_number':
33 case 'issuer_id':
34 case 'card_num':
35 case 'cvv':
36 case 'first_name':
37 case 'last_name':
38 case 'city':
39 case 'street_address':
40 case 'state_province':
41 case 'postal_code':
42 case 'expiration':
43 $error_token = $field;
44 break;
45 default:
46 $error_token = 'general';
47 break;
48 }
49 return $error_token;
50 }
51
60 public static function getEmptyErrorArray() {
61 return [
62 'general' => '',
63 'retryMsg' => '',
64 'amount' => '',
65 'card_num' => '',
66 'cvv' => '',
67 'fiscal_number' => '',
68 'first_name' => '',
69 'last_name' => '',
70 'city' => '',
71 'street_address' => '',
72 'state_province' => '',
73 'postal_code' => '',
74 'email' => '',
75 ];
76 }
77
93 public static function getError( $field, $type ) {
94 // NOTE: We are just using the next bit because it's convenient.
95 // getErrorToken is actually for something entirely different:
96 // Figuring out where on the form the error should land.
97 $token = self::getErrorToken( $field );
98
99 // Empty messages
100 if ( $type === 'not_empty' ) {
101 if ( $token != 'general' ) {
102 $missingErrorKey = "donate_interface-error-msg-{$token}";
103 return new ValidationError( $token, $missingErrorKey );
104 }
105 }
106
107 if ( $type === 'valid_type' || $type === 'calculated' ) {
108 $invalidErrorKey = "donate_interface-error-msg-invalid-{$token}";
109 return new ValidationError( $token, $invalidErrorKey );
110 }
111
112 // ultimate defaultness.
113 return new ValidationError( $token, 'donate_interface-error-msg-general' );
114 }
115
129 public static function validate( GatewayType $gateway, $data, $check_not_empty = [] ) {
130 // return the array of errors that should be generated on validate.
131 // just the same way you'd do it if you were a form passing the error array around.
132
146 // Define all default validations.
147 $validations = [
148 'not_empty' => [
149 'currency',
150 'gateway',
151 ],
152 'valid_type' => [
153 '_cache_' => 'validate_boolean',
154 'account_number' => 'validate_numeric',
155 'anonymous' => 'validate_boolean',
156 'contribution_tracking_id' => 'validate_numeric',
157 'currency' => 'validate_alphanumeric',
158 'gateway' => 'validate_alphanumeric',
159 'numAttempt' => 'validate_numeric',
160 'opt-in' => 'validate_boolean',
161 'posted' => 'validate_boolean',
162 'recurring' => 'validate_boolean',
163 ],
164 // Note that order matters for this group, dependencies must come first.
165 'calculated' => [
166 'gateway' => 'validate_gateway',
167 'address' => 'validate_address',
168 'city' => 'validate_address',
169 'email' => 'validate_email',
170 'street_address' => 'validate_address',
171 'postal_code' => 'validate_address',
172 'currency' => 'validate_currency_code',
173 'first_name' => 'validate_name',
174 'last_name' => 'validate_name',
175 'name' => 'validate_name',
176 'employer' => 'validate_name',
177 'employer_id' => 'validate_numeric',
178 ],
179 ];
180
181 // Additional fields we should check for emptiness.
182 if ( $check_not_empty ) {
183 $validations['not_empty'] = array_unique( array_merge(
184 $check_not_empty, $validations['not_empty']
185 ) );
186 }
187
188 $errors = [];
189 $errored_fields = [];
190 $results = [];
191
192 foreach ( $validations as $phase => $fields ) {
193 foreach ( $fields as $key => $custom ) {
194 // Here we decode list vs map elements.
195 if ( is_numeric( $key ) ) {
196 $field = $custom;
197 $validation_function = "validate_{$phase}";
198 } else {
199 $field = $key;
200 $validation_function = $custom;
201 }
202
203 $value = $data[$field] ?? null;
204 if ( empty( $value ) && $phase !== 'not_empty' ) {
205 // Skip if not required and nothing to validate.
206 continue;
207 }
208
209 // Skip if we've already determined this field group is invalid.
210 $errorToken = self::getErrorToken( $field );
211 if ( array_key_exists( $errorToken, $errored_fields ) ) {
212 continue;
213 }
214
215 // Prepare to call the thing.
216 $callable = [ 'DataValidator', $validation_function ];
217 if ( !is_callable( $callable ) ) {
218 throw new BadMethodCallException( __FUNCTION__ . " BAD PROGRAMMER. No function {$validation_function} for $field" );
219 }
220 $result = null;
221 // Handle special cases.
222 switch ( $validation_function ) {
223 case 'validate_currency_code':
224 $result = call_user_func( $callable, $value, $gateway->getCurrencies( $data ) );
225 break;
226 default:
227 $result = call_user_func( $callable, $value );
228 break;
229 }
230
231 // Store results.
232 $results[$phase][$field] = $result;
233 if ( $result === false ) {
234 // We did the check, and it failed.
235 $errored_fields[$errorToken] = true;
236 $errors[] = self::getError( $field, $phase );
237 }
238 }
239 }
240
241 return $errors;
242 }
243
255 protected static function checkValidationPassed( $fields, $results ) {
256 foreach ( $fields as $field ) {
257 foreach ( $results as $phase => $results_fields ) {
258 if ( array_key_exists( $field, $results_fields )
259 && $results_fields[$field] !== true
260 ) {
261 return false;
262 }
263 }
264 }
265 return true;
266 }
267
275 protected static function validate_email( $value ) {
276 return WmfFramework::validateEmail( $value )
279 }
280
281 protected static function validate_currency_code( $value, $acceptedCurrencies ) {
282 if ( !$value ) {
283 return false;
284 }
285
286 return in_array( $value, $acceptedCurrencies );
287 }
288
295 protected static function validate_credit_card( $value ) {
296 $calculated_card_type = self::getCardType( $value );
297 if ( !$calculated_card_type ) {
298 return false;
299 }
300 return true;
301 }
302
309 protected static function validate_boolean( $value ) {
310 $validValues = [
311 0,
312 '0',
313 false,
314 'false',
315 1,
316 '1',
317 true,
318 'true',
319 ];
320 return in_array( $value, $validValues, true );
321 }
322
329 protected static function validate_numeric( $value ) {
330 // instead of validating here, we should probably be doing something else entirely.
331 if ( is_numeric( $value ) ) {
332 return true;
333 }
334 return false;
335 }
336
344 protected static function validate_gateway( $value ) {
345 global $wgDonationInterfaceGatewayAdapters;
346
347 return array_key_exists( $value, $wgDonationInterfaceGatewayAdapters );
348 }
349
358 protected static function validate_not_empty( $value ) {
359 return ( $value !== null && $value !== '' );
360 }
361
371 protected static function validate_alphanumeric( $value ) {
372 return true;
373 }
374
383 public static function validate_not_just_punctuation( $value ) {
384 $value = html_entity_decode( $value ); // Just making sure.
385 $regex = '/([\x20-\x2F]|[\x3A-\x40]|[\x5B-\x60]|[\x7B-\x7E]){' . strlen( $value ) . '}/';
386 if ( preg_match( $regex, $value ) ) {
387 return false;
388 }
389 return true;
390 }
391
399 public static function validate_name( $value ) {
400 return !self::cc_number_exists_in_str( $value ) &&
401 !self::obviousXssInString( $value );
402 }
403
409 public static function validate_address( $value ) {
410 return !self::cc_number_exists_in_str( $value ) &&
411 !self::obviousXssInString( $value );
412 }
413
414 public static function obviousXssInString( $value ) {
415 return ( strpos( $value, '>' ) !== false ) ||
416 ( strpos( $value, '<' ) !== false );
417 }
418
427 public static function special_characters_in_wrong_locations( $str ) {
428 $specialCharacterRegex = <<<EOT
429/
430(^[\-])|
431([`!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~]+$)
432/
433EOT;
434 // Transform the regex to get rid of the new lines
435 $specialCharacterRegex = preg_replace( '/\s/', '', $specialCharacterRegex );
436 $matches = [];
437 if ( preg_match_all( $specialCharacterRegex, $str, $matches ) > 0 ) {
438 return true;
439 }
440 return false;
441 }
442
450 public static function cc_number_exists_in_str( string $str ): bool {
451 $luhnRegex = <<<EOT
452/
453(?#amex)(3[47][0-9]{13})|
454(?#bankcard)(5610[0-9]{12})|(56022[1-5][0-9]{10})|
455(?#diners carte blanche)(300[0-5][0-9]{11})|
456(?#diners intl)(36[0-9]{12})|
457(?#diners US CA)(5[4-5][0-9]{14})|
458(?#discover)(6011[0-9]{12})|(622[0-9]{13})|(64[4-5][0-9]{13})|(65[0-9]{14})|
459(?#InstaPayment)(63[7-9][0-9]{13})|
460(?#JCB)(35[2-8][0-9]{13})|
461(?#Laser)(6(304|7(06|09|71))[0-9]{12,15})|
462(?#Maestro)((5018|5020|5038|5893|6304|6759|6761|6762|6763|0604)[0-9]{8,15})|
463(?#Mastercard)(5[1-5][0-9]{14})|
464(?#Solo)((6334|6767)[0-9]{12,15})|
465(?#Switch)((4903|4905|4911|4936|6333|6759)[0-9]{12,15})|((564182|633110)[0-9]{10,13})|
466(?#Visa)(4([0-9]{15}|[0-9]{12}))
467/
468EOT;
469
470 $nonLuhnRegex = <<<EOT
471/
472(?#china union pay)(62[0-9]{14,17})|
473(?#diners enroute)((2014|2149)[0-9]{11})
474/
475EOT;
476
477 // Transform the regex to get rid of the new lines
478 $luhnRegex = preg_replace( '/\s/', '', $luhnRegex );
479 $nonLuhnRegex = preg_replace( '/\s/', '', $nonLuhnRegex );
480
481 // Remove common CC# delimiters
482 $str = preg_replace( '/[\s\-]/', '', $str );
483
484 // Now split the string on everything else and implode again so the regexen have an 'easy' time
485 $str = implode( ' ', preg_split( '/[^0-9]+/', $str, PREG_SPLIT_NO_EMPTY ) );
486
487 // First do we have any numbers that match a pattern but is not luhn checkable?
488 $matches = [];
489 if ( preg_match_all( $nonLuhnRegex, $str, $matches ) > 0 ) {
490 return true;
491 }
492
493 // Find potential CC numbers that do luhn check and run 'em
494 $matches = [];
495 preg_match_all( $luhnRegex, $str, $matches );
496 foreach ( $matches[0] as $candidate ) {
497 if ( self::luhn_check( $candidate ) ) {
498 return true;
499 }
500 }
501
502 // All our checks have failed; probably doesn't contain a CC number
503 return false;
504 }
505
513 public static function luhn_check( $str ) {
514 $odd = ( strlen( $str ) % 2 );
515 $sum = 0;
516 $len = strlen( $str );
517
518 for ( $i = 0; $i < $len; $i++ ) {
519 if ( $odd ) {
520 $sum += $str[$i];
521 } else {
522 if ( ( $str[$i] * 2 ) > 9 ) {
523 $sum += $str[$i] * 2 - 9;
524 } else {
525 $sum += $str[$i] * 2;
526 }
527 }
528
529 $odd = !$odd;
530 }
531 return( ( $sum % 10 ) == 0 );
532 }
533
539 public static function getCardType( $card_num ) {
540 // validate that credit card number entered is correct and set the card type
541 if ( preg_match( '/^3[47][0-9]{13}$/', $card_num ) ) { // american express
542 return 'amex';
543 } elseif ( preg_match( '/^5[1-5][0-9]{14}$/', $card_num ) ) { // mastercard
544 return 'mc';
545 } elseif ( preg_match( '/^4[0-9]{12}(?:[0-9]{3})?$/', $card_num ) ) {// visa
546 return 'visa';
547 } elseif ( preg_match( '/^6(?:011|5[0-9]{2})[0-9]{12}$/', $card_num ) ) { // discover
548 return 'discover';
549 } else { // an unrecognized card type was entered
550 return false;
551 }
552 }
553
563 public static function guessLanguage( $data ) {
564 if ( array_key_exists( 'language', $data )
565 && WmfFramework::isValidBuiltInLanguageCode( $data['language'] ) ) {
566 return $data['language'];
567 } else {
568 return WmfFramework::getLanguageCode();
569 }
570 }
571
579 public static function expandIPBlockToArray( $ip ) {
580 $parts = explode( '/', $ip );
581 if ( count( $parts ) === 1 ) {
582 return [ $ip ];
583 } else {
584 // expand that mess.
585 // this next bit was stolen from php.net and smacked around some
586 $corr = ( pow( 2, 32 ) - 1 ) - ( pow( 2, 32 - $parts[1] ) - 1 );
587 $first = ip2long( $parts[0] ) & ( $corr );
588 $length = pow( 2, 32 - $parts[1] ) - 1;
589 $ips = [];
590 for ( $i = 0; $i <= $length; $i++ ) {
591 $ips[] = long2ip( $first + $i );
592 }
593 return $ips;
594 }
595 }
596
606 public static function ip_is_listed( $ip, $ip_list ) {
607 $expanded = [];
608 if ( empty( $ip_list ) ) {
609 return false;
610 }
611 foreach ( $ip_list as $address ) {
612 $expanded = array_merge( $expanded, self::expandIPBlockToArray( $address ) );
613 }
614
615 return in_array( $ip, $expanded, true );
616 }
617
628 public static function value_appears_in( $needle, $haystack ) {
629 $needle = ( is_array( $needle ) ) ? $needle : [ $needle ];
630 $haystack = ( is_array( $haystack ) ) ? $haystack : [ $haystack ];
631
632 $plusCheck = array_key_exists( '+', $haystack );
633 $minusCheck = array_key_exists( '-', $haystack );
634
635 if ( $plusCheck || $minusCheck ) {
636 // With +/- checks we will first explicitly deny anything in '-'
637 // Then if '+' is defined accept anything there
638 // but if '+' is not defined we just let everything that wasn't denied by '-' through
639 // Otherwise we assume both were defined and deny everything :)
640
641 if ( $minusCheck && self::value_appears_in( $needle, $haystack['-'] ) ) {
642 return false;
643 }
644 if ( $plusCheck && self::value_appears_in( $needle, $haystack['+'] ) ) {
645 return true;
646 } elseif ( !$plusCheck ) {
647 // Implicit acceptance
648 return true;
649 }
650 return false;
651 }
652
653 if ( ( count( $haystack ) === 1 ) && ( in_array( 'ALL', $haystack ) ) ) {
654 // If the haystack can accept anything, then whoo!
655 return true;
656 }
657
658 $haystack = array_filter( $haystack, static function ( $value ) {
659 return !is_array( $value );
660 } );
661 $result = array_intersect( $haystack, $needle );
662 if ( !empty( $result ) ) {
663 return true;
664 } else {
665 return false;
666 }
667 }
668
676 public static function getZeroPaddedValue( $value, $total_length ) {
677 // first, trim all leading zeroes off the value.
678 $ret = ltrim( $value, '0' );
679
680 // now, check to see if it's going to be a valid value at all,
681 // and give up if it's hopeless.
682 if ( strlen( $ret ) > $total_length ) {
683 return false;
684 }
685
686 // ...and if we're still here, left pad with zeroes to required length
687 $ret = str_pad( $ret, $total_length, '0', STR_PAD_LEFT );
688
689 return $ret;
690 }
691
692}
DataValidator This class is responsible for performing all kinds of data validation,...
static getZeroPaddedValue( $value, $total_length)
Okay, so this isn't all validation, but there's a validation component in there so I'm calling it clo...
static expandIPBlockToArray( $ip)
Takes either an IP address, or an IP address with a CIDR block, and expands it to an array containing...
static ip_is_listed( $ip, $ip_list)
Check whether IP matches a block list.
static validate_not_just_punctuation( $value)
Validates that somebody didn't just punch in a bunch of punctuation, and nothing else.
static getCardType( $card_num)
Calculates and returns the card type for a given credit card number.
static validate_numeric( $value)
validate_numeric Determines if the $value passed in is numeric.
static validate_alphanumeric( $value)
validate_alphanumeric Checks to make sure the value is populated with an alphanumeric value....
static validate_name( $value)
Some people are silly and enter their CC numbers as their name.
static validate_gateway( $value)
validate_gateway Checks to make sure the gateway is populated with a valid and enabled gateway.
static getEmptyErrorArray()
getEmptyErrorArray
static getErrorToken( $field)
getErrorToken, intended to be used by classes that exist relatively close to the form classes,...
static validate_not_empty( $value)
validate_not_empty Checks to make sure that the $value is present in the $data array,...
static validate_credit_card( $value)
validate_credit_card Determines if the $value passed in is (possibly) a valid credit card number.
static validate_boolean( $value)
validate_boolean Determines if the $value passed in is a valid boolean.
static getError( $field, $type)
getError - returns the error object appropriate for a validation error on the specified field,...
static value_appears_in( $needle, $haystack)
Test to determine if a value appears in a haystack.
static special_characters_in_wrong_locations( $str)
Analyzes a string to see if there are special characters at the beginning/end of a string or right be...
static guessLanguage( $data)
Returns a valid mediawiki language code to use for all the DonationInterface translations.
static luhn_check( $str)
Performs a Luhn algorithm check on a string.
static validate_address( $value)
Gets rid of numbers that pass luhn in address fields -.
static validate_currency_code( $value, $acceptedCurrencies)
static checkValidationPassed( $fields, $results)
checkValidationPassed is a validate helper function.
static cc_number_exists_in_str(string $str)
Analyzes a string to see if any credit card numbers are hiding out in it.
static validate_email( $value)
validate_email Determines if the $value passed in is a valid email address.
static validate(GatewayType $gateway, $data, $check_not_empty=[])
validate Run all the validation rules we have defined against a (hopefully normalized) DonationInterf...
static obviousXssInString( $value)
GatewayType Interface.
getCurrencies( $options=[])