Newer
Older
Ryan Szrama
committed
<?php
/**
* @file
* Credit-card helper functions for Drupal commerce.
*/
/**
* Returns a set of credit card form elements that payment method modules can
* incorporate into their submission form callbacks.
Ryan Szrama
committed
*
* @param $fields
Ryan Szrama
committed
* An associative array specifying the fields that should be included on the
* credit card form. The card number and expiration fields are always present,
* and fields whose array keys listed below aren't set will be left out of the
* credit card form:
* - type: an array identifying supported card types using the keys of the
* return array from commerce_payment_credit_card_types().
* - owner: TRUE to include an owner name textfield.
* - start_date: TRUE to include start date select lists.
* - issue: boolean that when present enables an issue number field; if TRUE,
* makes the field required; if FALSE, makes the field optional.
* - code: text label to use for a security code / CVV textfield.
* - bank: TRUE to include a bank name textfield.
Ryan Szrama
committed
* @param $default
* An array of default values for the available CC fields.
Ryan Szrama
committed
*
* @return
* A credit card form array for use in another form.
Ryan Szrama
committed
*/
function commerce_payment_credit_card_form($fields = array(), $default = array()) {
Ryan Szrama
committed
// Merge default values into the default array.
$default += array(
'type' => '',
'owner' => '',
'number' => '',
'start_month' => '',
'start_year' => date('Y') - 5,
'exp_month' => date('m'),
'exp_year' => date('Y'),
'issue' => '',
'code' => '',
'bank' => '',
);
Ryan Szrama
committed
$current_year_2 = date('y');
$current_year_4 = date('Y');
$form['credit_card'] = array(
'#tree' => TRUE,
'#attached' => array(
'css' => array(drupal_get_path('module', 'commerce_payment') . '/theme/commerce_payment.theme.css'),
),
Ryan Szrama
committed
);
Ryan Szrama
committed
// Add a card type selector if specified.
if (isset($fields['type'])) {
$form['credit_card']['type'] = array(
Ryan Szrama
committed
'#type' => 'select',
'#title' => t('Card type'),
'#options' => array_intersect_key(commerce_payment_credit_card_types(), drupal_map_assoc((array) $fields['type'])),
'#default_value' => $default['type'],
Ryan Szrama
committed
);
Ryan Szrama
committed
$form['credit_card']['valid_types'] = array(
'#type' => 'value',
'#value' => $fields['type'],
);
}
else {
$form['credit_card']['valid_types'] = array(
'#type' => 'value',
'#value' => array(),
);
Ryan Szrama
committed
}
// Add a field for the credit card owner if specified.
if (isset($fields['owner'])) {
$form['credit_card']['owner'] = array(
Ryan Szrama
committed
'#type' => 'textfield',
'#title' => t('Card owner'),
'#default_value' => $default['owner'],
Ryan Szrama
committed
'#attributes' => array('autocomplete' => 'off'),
'#required' => TRUE,
'#maxlength' => 64,
'#size' => 32,
);
}
// Always add a field for the credit card number.
$form['credit_card']['number'] = array(
Ryan Szrama
committed
'#type' => 'textfield',
'#title' => t('Card number'),
'#default_value' => $default['number'],
Ryan Szrama
committed
'#attributes' => array('autocomplete' => 'off'),
'#required' => TRUE,
'#maxlength' => 19,
'#size' => 20,
);
// Add fields for the credit card start date if specified.
if (isset($fields['start_date'])) {
$form['credit_card']['start_month'] = array(
Ryan Szrama
committed
'#type' => 'select',
'#title' => t('Start date'),
'#options' => drupal_map_assoc(array_keys(commerce_months())),
Ryan Szrama
committed
'#default_value' => strlen($default['start_month']) == 1 ? '0' . $default['start_month'] : $default['start_month'],
'#required' => TRUE,
'#prefix' => '<div class="commerce-credit-card-start">',
'#suffix' => '<span class="commerce-month-year-divider">/</span>',
Ryan Szrama
committed
);
// Build a year select list that uses a 4 digit key with a 2 digit value.
$options = array();
for ($i = -10; $i < 1; $i++) {
$options[$current_year_4 + $i] = str_pad($current_year_2 + $i, 2, '0', STR_PAD_LEFT);
}
$form['credit_card']['start_year'] = array(
Ryan Szrama
committed
'#type' => 'select',
'#options' => $options,
'#default_value' => $default['start_year'],
'#suffix' => '</div>',
Ryan Szrama
committed
);
}
// Always add fields for the credit card expiration date.
$form['credit_card']['exp_month'] = array(
Ryan Szrama
committed
'#type' => 'select',
'#title' => t('Expiration'),
'#options' => drupal_map_assoc(array_keys(commerce_months())),
Ryan Szrama
committed
'#default_value' => strlen($default['exp_month']) == 1 ? '0' . $default['exp_month'] : $default['exp_month'],
'#required' => TRUE,
'#prefix' => '<div class="commerce-credit-card-expiration">',
'#suffix' => '<span class="commerce-month-year-divider">/</span>',
Ryan Szrama
committed
);
// Build a year select list that uses a 4 digit key with a 2 digit value.
$options = array();
for ($i = 0; $i < 20; $i++) {
$options[$current_year_4 + $i] = str_pad($current_year_2 + $i, 2, '0', STR_PAD_LEFT);
}
$form['credit_card']['exp_year'] = array(
Ryan Szrama
committed
'#type' => 'select',
'#options' => $options,
'#default_value' => $default['exp_year'],
'#suffix' => '</div>',
Ryan Szrama
committed
);
// Add a field for the card issue number if specified.
if (isset($fields['issue'])) {
$form['credit_card']['issue'] = array(
Ryan Szrama
committed
'#type' => 'textfield',
Julien Dubois
committed
'#title' => t('Issue number', array(), array('context' => 'credit card issue number for card types that require it')),
'#default_value' => $default['issue'],
Ryan Szrama
committed
'#attributes' => array('autocomplete' => 'off'),
'#required' => empty($fields['issue']) ? FALSE : TRUE,
Ryan Szrama
committed
'#maxlength' => 2,
'#size' => 2,
);
}
// Add a field for the security code if specified.
if (isset($fields['code'])) {
$form['credit_card']['code'] = array(
Ryan Szrama
committed
'#type' => 'textfield',
'#title' => !empty($fields['code']) ? $fields['code'] : t('Security code'),
'#default_value' => $default['code'],
Ryan Szrama
committed
'#attributes' => array('autocomplete' => 'off'),
'#required' => TRUE,
'#maxlength' => 4,
'#size' => 4,
);
}
// Add a field for the issuing bank if specified.
if (isset($fields['bank'])) {
$form['credit_card']['bank'] = array(
Ryan Szrama
committed
'#type' => 'textfield',
'#title' => t('Issuing bank'),
'#default_value' => $default['bank'],
Ryan Szrama
committed
'#attributes' => array('autocomplete' => 'off'),
'#required' => TRUE,
'#maxlength' => 64,
'#size' => 32,
);
}
return $form;
}
/**
* Validates a set of credit card details entered via the credit card form.
*
* @param $details
* An array of credit card details as retrieved from the credit card array in
Ryan Szrama
committed
* the form values of a form containing the credit card form.
* @param $settings
* Settings used for calling validation functions and setting form errors:
* - form_parents: an array of parent elements identifying where the credit
* card form was situated in the form array
*
* @return
* TRUE or FALSE indicating the validity of all the data.
*
* @see commerce_payment_credit_card_form()
*/
function commerce_payment_credit_card_validate($details, $settings) {
$prefix = implode('][', $settings['form_parents']) . '][';
$valid = TRUE;
Damien McKenna
committed
// Validate the credit card number.
if (!commerce_payment_validate_credit_card_number($details['number'])) {
form_set_error($prefix . 'number', t('You have entered an invalid credit card number.'));
$valid = FALSE;
}
// Validate the credit card type if the credit card number is valid.
elseif (!empty($details['valid_types'])) {
$type = commerce_payment_validate_credit_card_type($details['number'], $details['valid_types']);
if ($type === FALSE) {
form_set_error($prefix . 'type', t('You have entered a credit card number of an unsupported card type.'));
$valid = FALSE;
}
elseif ($type != $details['type']) {
form_set_error($prefix . 'number', t('You have entered a credit card number that does not match the type selected.'));
$valid = FALSE;
}
Ryan Szrama
committed
}
// Validate the expiration date.
if (($invalid = commerce_payment_validate_credit_card_exp_date($details['exp_month'], $details['exp_year'])) !== TRUE) {
form_set_error($prefix . 'exp_' . $invalid, t('You have entered an expired credit card.'));
$valid = FALSE;
}
// Validate the security code if present.
Jonathan Sacksick
committed
if (!empty($details['code']) && !commerce_payment_validate_credit_card_security_code($details['number'], $details['code'])) {
form_set_error($prefix . 'code', t('You have entered an invalid card security code.'));
$valid = FALSE;
}
// Validate the start date if present.
Ryan Szrama
committed
if (isset($details['start_month']) && ($invalid = commerce_payment_validate_credit_card_start_date($details['start_month'], $details['start_year'])) !== TRUE) {
form_set_error($prefix . 'start_' . $invalid, t('Your have entered an invalid start date.'));
$valid = FALSE;
}
// Validate the issue number if present.
if (isset($details['issue']) && !commerce_payment_validate_credit_card_issue($details['issue'])) {
form_set_error($prefix . 'issue', t('You have entered an invalid issue number.'));
$valid = FALSE;
}
return $valid;
}
/**
Ryan Szrama
committed
* Validates a credit card number using an array of approved card types.
*
Matt Glaman
committed
* @param int $number
* The credit card number to validate.
Matt Glaman
committed
* @param array $card_types
* An array of credit card types containing any of the keys from the array
Ryan Szrama
committed
* returned by commerce_payment_credit_card_types(). Only numbers determined
* to be of the types specified will pass validation. This determination is
* based on the length of the number and the valid number ranges for the
* various types of known credit card types.
*
* @return
* FALSE if a number is not valid based on approved credit card types or the
Matt Glaman
committed
* credit card type if it is valid and could be determined.
*
Ryan Szrama
committed
* @see http://en.wikipedia.org/wiki/Bank_card_number#Issuer_Identification_Number_.28IIN.29
* @see commerce_payment_credit_card_types()
*/
Matt Glaman
committed
function commerce_payment_validate_credit_card_type($number, array $card_types = array()) {
Jonathan Sacksick
committed
$type = CommercePaymentCreditCard::detectType($number);
if (!$type || !in_array($type['id'], $card_types)) {
return FALSE;
}
return $type['id'];
Ryan Szrama
committed
}
Ryan Szrama
committed
Ryan Szrama
committed
/**
* Validates a credit card number using the Luhn algorithm.
*
* @param $number
* The credit card number to validate.
*
* @return
* TRUE or FALSE indicating the number's validity.
*
* @see http://www.merriampark.com/anatomycc.htm
*/
function commerce_payment_validate_credit_card_number($number) {
Matt Glaman
committed
$type = CommercePaymentCreditCard::detectType($number);
Kosta Harlan
committed
if (!$type || !is_array($type)) {
return FALSE;
}
Matt Glaman
committed
return CommercePaymentCreditCard::validateNumber($number, $type);
}
/**
* Validates a credit card start date.
*
Matt Glaman
committed
* @param int $month
* The 1 or 2-digit numeric representation of the month, i.e. 1, 6, 12.
Matt Glaman
committed
* @param int $year
* The 4-digit numeric representation of the year, i.e. 2010.
*
* @return
* TRUE for cards whose start date is blank (both month and year) or in the
Ryan Szrama
committed
* past, 'year' or 'month' for expired cards indicating which value should
* receive the error.
*/
function commerce_payment_validate_credit_card_start_date($month, $year) {
if (empty($month) && empty($year)) {
return TRUE;
}
if (empty($month) || empty($year)) {
return empty($month) ? 'month' : 'year';
}
Ryan Szrama
committed
if ($month < 1 || $month > 12) {
return 'month';
}
if ($year > date('Y')) {
return 'year';
}
elseif ($year == date('Y')) {
if ($month > date('n')) {
return 'month';
}
}
return TRUE;
}
/**
* Validates a credit card expiration date.
*
Matt Glaman
committed
* @param int $month
* The 1 or 2-digit numeric representation of the month, i.e. 1, 6, 12.
Matt Glaman
committed
* @param int $year
* The 4-digit numeric representation of the year, i.e. 2010.
*
* @return
* TRUE for non-expired cards, 'year' or 'month' for expired cards indicating
Ryan Szrama
committed
* which value should receive the error.
*/
function commerce_payment_validate_credit_card_exp_date($month, $year) {
Matt Glaman
committed
return CommercePaymentCreditCard::validateExpirationDate($month, $year);
}
/**
* Validates that an issue number is numeric if present.
*/
function commerce_payment_validate_credit_card_issue($issue) {
if (empty($issue) || (is_numeric($issue) && $issue > 0)) {
return TRUE;
}
return FALSE;
}
/**
* Validates a card security code based on the type of the credit card.
*
* @param $number
* The number of the credit card to validate the security code against.
* @param $code
* The card security code to validate with the given number.
*
* @return
* TRUE or FALSE indicating the security code's validity.
*/
function commerce_payment_validate_credit_card_security_code($number, $code) {
Matt Glaman
committed
$type = CommercePaymentCreditCard::detectType($number);
Kosta Harlan
committed
if (!$type || !is_array($type)) {
return FALSE;
}
Matt Glaman
committed
return CommercePaymentCreditCard::validateSecurityCode($code, $type);
Jonathan Sacksick
committed
}
Matt Glaman
committed
Jonathan Sacksick
committed
/**
* Returns an associative array of credit card types.
Matt Glaman
committed
*
* Provides BC layer for CommercePaymentCreditCard::getTypeLabels().
*
* @see CommercePaymentCreditCard::getTypeLabels()
*
* @return array
* An array keyed by card type with card type label.
Jonathan Sacksick
committed
*/
function commerce_payment_credit_card_types() {
Matt Glaman
committed
return CommercePaymentCreditCard::getTypeLabels();
}
/**
* Provides logic for listing card types and validating card details.
*/
final class CommercePaymentCreditCard {
/**
Ryan Szrama
committed
* Gets all available card types.
Matt Glaman
committed
*
* @return array
Ryan Szrama
committed
* The array of card types, keyed by card type ID.
Matt Glaman
committed
*/
public static function getTypes() {
$definitions = array(
'visa' => array(
'id' => 'visa',
'label' => t('Visa'),
'number_prefixes' => array('4'),
Bojan Živanović
committed
'number_lengths' => array(16, 18, 19),
Matt Glaman
committed
),
'mastercard' => array(
'id' => 'mastercard',
Bojan Živanović
committed
'label' => t('Mastercard'),
Matt Glaman
committed
'number_prefixes' => array('51-55', '222100-272099'),
),
'maestro' => array(
'id' => 'maestro',
'label' => t('Maestro'),
'number_prefixes' => array(
Bojan Živanović
committed
'5018', '502', '503', '506', '56', '58', '639', '6220', '67',
Matt Glaman
committed
),
'number_lengths' => array(12, 13, 14, 15, 16, 17, 18, 19),
),
'amex' => array(
'id' => 'amex',
'label' => t('American Express'),
'number_prefixes' => array('34', '37'),
'number_lengths' => array(15),
'security_code_length' => 4,
),
'dc' => array(
'id' => 'dc',
'label' => t('Diners Club'),
'number_prefixes' => array('300-305', '309', '36', '38', '39'),
Bojan Živanović
committed
'number_lengths' => array(14, 16, 19),
Matt Glaman
committed
),
'discover' => array(
'id' => 'discover',
'label' => t('Discover Card'),
'number_prefixes' => array('6011', '622126-622925', '644-649', '65'),
'number_lengths' => array(16, 19),
),
'jcb' => array(
'id' => 'jcb',
'label' => t('JCB'),
'number_prefixes' => array('3528-3589'),
Bojan Živanović
committed
'number_lengths' => array(16, 17, 18, 19),
Matt Glaman
committed
),
'unionpay' => array(
'id' => 'unionpay',
'label' => t('UnionPay'),
'number_prefixes' => array('62', '88'),
'number_lengths' => array(16, 17, 18, 19),
'uses_luhn' => FALSE,
),
);
Ryan Szrama
committed
drupal_alter('commerce_card_type_info', $definitions);
Matt Glaman
committed
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
foreach ($definitions as &$definition) {
$definition += array(
'number_lengths' => array(16),
'security_code_length' => 3,
'uses_luhn' => TRUE,
);
}
return $definitions;
}
/**
* Gets the labels of all available credit card types.
*
* @return array
* The labels, keyed by ID.
*/
public static function getTypeLabels() {
$type_labels = array();
foreach (self::getTypes() as $type) {
$type_labels[$type['id']] = $type['label'];
}
return $type_labels;
}
/**
* Detects the credit card type based on the number.
*
* @param string $number
* The credit card number.
*
* @return array|false
* The credit card type, or NULL if unknown.
*/
public static function detectType($number) {
if (!is_numeric($number)) {
return FALSE;
}
$types = self::getTypes();
foreach ($types as $type) {
foreach ($type['number_prefixes'] as $prefix) {
if (self::matchPrefix($number, $prefix)) {
return $type;
}
}
}
return FALSE;
}
/**
* Checks whether the given credit card number matches the given prefix.
*
* @param string $number
* The credit card number.
* @param string $prefix
* The prefix to match against. Can be a single number such as '43' or a
* range such as '30-35'.
*
* @return bool
* TRUE if the credit card number matches the prefix, FALSE otherwise.
*/
public static function matchPrefix($number, $prefix) {
if (is_numeric($prefix)) {
return substr($number, 0, strlen($prefix)) == $prefix;
}
else {
list($start, $end) = explode('-', $prefix);
$number = substr($number, 0, strlen($start));
return $number >= $start && $number <= $end;
}
}
/**
* Validates the given credit card number.
*
* @param string $number
* The credit card number.
* @param array $type
* The credit card type.
*
* @return bool
* TRUE if the credit card number is valid, FALSE otherwise.
*/
public static function validateNumber($number, array $type) {
if (!is_numeric($number)) {
return FALSE;
}
if (!in_array(strlen($number), $type['number_lengths'])) {
return FALSE;
}
if ($type['uses_luhn'] && !self::validateLuhn($number)) {
return FALSE;
}
return TRUE;
}
/**
* Validates the given credit card number using the Luhn algorithm.
*
* @param string $number
* The credit card number.
*
* @return bool
* TRUE if the credit card number is valid, FALSE otherwise.
*/
public static function validateLuhn($number) {
$total = 0;
foreach (array_reverse(str_split($number)) as $i => $digit) {
$digit = $i % 2 ? $digit * 2 : $digit;
$digit = $digit > 9 ? $digit - 9 : $digit;
$total += $digit;
}
return ($total % 10 === 0);
}
/**
* Validates the given credit card expiration date.
*
* @param string $month
* The 1 or 2-digit numeric representation of the month, i.e. 1, 6, 12.
* @param string $year
* The 4-digit numeric representation of the year, i.e. 2010.
*
* @return bool
* TRUE if the credit card expiration date is valid, FALSE otherwise.
*/
public static function validateExpirationDate($month, $year) {
if ($month < 1 || $month > 12) {
return FALSE;
}
if ($year < date('Y')) {
return FALSE;
}
elseif ($year == date('Y') && $month < date('n')) {
return FALSE;
}
return TRUE;
}
/**
* Calculates the unix timestamp for a credit card expiration date.
*
* @param string $month
* The 1 or 2-digit numeric representation of the month, i.e. 1, 6, 12.
* @param string $year
* The 4-digit numeric representation of the year, i.e. 2010.
*
* @return int
* The expiration date as a unix timestamp.
*/
public static function calculateExpirationTimestamp($month, $year) {
// Credit cards expire on the last day of the month.
$month_start = strtotime($year . '-' . $month . '-01');
$last_day = date('t', $month_start);
return strtotime($year . '-' . $month . '-' . $last_day);
}
/**
* Validates the given credit card security code.
*
* @param string $security_code
* The credit card security code.
* @param array $type
* The credit card type.
*
* @return bool
* TRUE if the credit card security code is valid, FALSE otherwise.
*/
public static function validateSecurityCode($security_code, array $type) {
if (!is_numeric($security_code)) {
return FALSE;
}
if (strlen($security_code) != $type['security_code_length']) {
return FALSE;
}
return TRUE;
}