Example #1
0
 public function validate($server)
 {
     $this->clearErrors();
     $metaData = Transaction::get('array');
     // Check the currency is a valid one
     if (!Metadata\Iso4217::checkCurrency($server->getField('Currency'))) {
         $this->addError('Currency', $this->CURRENCY_INVALID);
     }
     $this->validateAmount($server->getField('Amount'));
     // Perform some general validations and return ourself
     return parent::validate($server);
 }
Example #2
0
 public function validate($item)
 {
     $metaData = Transaction::get('array');
     foreach ($this->fieldsToCheck as $field) {
         if (is_a($this, 'Academe\\SagePay\\Validator\\Model\\Address')) {
             // I'm assuming/hoping that Billing and Delivery validation rules are identical
             $data = $metaData['Billing' . $field];
         } else {
             $data = $metaData[$field];
         }
         $value = $item->getField($field);
         if ($this->hasError($field)) {
             // We only store one error per field, so if we have already got an error for this
             // field then don't waste time checking others. This also means that we can have
             // more specific fields in the child objects which over-ride these completly.
             continue;
         }
         // State is only used when the country is US, the validation rules stores assume country is US.
         // so here we tweak them.
         if ($field == 'State' && $item->getField('Country') != 'US') {
             $data['required'] = false;
             $data['min'] = 0;
             $data['max'] = 0;
         }
         // If the item is required, check it's not empty
         if ($data['required'] && !v::notEmpty()->validate($value)) {
             if ($field == 'PostCode') {
                 // Add an exception for Postcodes when the country is one which does not have postcodes
                 if (!in_array($item->getField('Country'), $this->countriesWhichDontHavePostcodes)) {
                     $this->addError($field, sprintf($this->CANNOT_BE_EMPTY, $field));
                 }
             } else {
                 if ($field == 'Amount') {
                     // 0 equates to empty, so do a special check
                     if ($item->getField('Amount') != '0' && !v::string()->notEmpty()->validate($item->getField('Amount'))) {
                         $this->addError($field, sprintf($this->CANNOT_BE_EMPTY, $field));
                     }
                 } else {
                     $this->addError($field, sprintf($this->CANNOT_BE_EMPTY, $field));
                 }
             }
         }
         // If there is a minimum or maximum check the length.
         // TODO: Check whether this code works well when only one or the other is set
         $min = isset($data['min']) ? $data['min'] : null;
         $max = isset($data['max']) ? $data['max'] : null;
         if ($min != null && $max != null) {
             // Check the length of the field
             if ($field == 'State') {
                 print_r("\n\nMin: {$field} : {$min}, \n\n");
                 die;
             }
             if (!v::length($min, $max, true)->validate($value)) {
                 if ($min == $max) {
                     $this->addError($field, sprintf($this->BAD_LENGTH, $field, $min));
                 } else {
                     $this->addError($field, sprintf($this->BAD_RANGE, $field, $min, $max));
                 }
             }
         }
         // Check the contents of the field
         if (isset($data['chars'])) {
             // We build two regexes, one for testing whether it matches and the other for
             // filtering out the bad characters to show the user which are not valid.
             $regex = $this->buildRegex($data['chars']);
             try {
                 if (!v::regex($regex)->validate($value)) {
                     $cleanupRegex = $this->buildRegex($data['chars'], false);
                     $badChars = preg_replace($cleanupRegex, '', $value);
                     $this->addError($field, sprintf($this->BAD_CHARACTERS, $field, $badChars, $regex));
                 }
             } catch (\Exception $e) {
                 throw new \Exception("preg_match has a problem with this regex '{$regex}'");
             }
         }
     }
     return $this;
 }
Example #3
0
 /**
  * Collect the query data together for the transaction registration.
  * Returns key/value pairs
  * Different types of transaction will need different sets of fields to
  * be sent to SagePay. This is indicated by the $message_type.
  */
 public function queryData($format_as_querystring = true, $message_type = 'server-registration')
 {
     $this->checkTxModel();
     // Make sure all the models are expanded into the main transaction data model.
     $this->expandModels();
     $query = array();
     // Get the list of fields we want to send to SagePay from the transaction metadata.
     // Filter the list by the message type. This is the superset of fields.
     $all_fields = Metadata\Transaction::get('array', array('source' => $message_type));
     $fields_to_send = array();
     $optional_fields = array();
     foreach ($all_fields as $field_name => $field_meta) {
         $fields_to_send[] = $field_name;
         if (empty($field_meta['required'])) {
             $optional_fields[] = $field_name;
         }
     }
     // Some fields will need renaming before sending to SagePay. Do some prelimiary checks first.
     $TxType = $this->getField('TxType');
     $rename_vendor_tx_code = $TxType == 'RELEASE' || $TxType == 'ABORT' || $TxType == 'VOID' || $TxType == 'CANCEL';
     // Loop through the fields, both optional and mandatory.
     foreach ($fields_to_send as $field) {
         $value = $this->getField($field);
         if (!in_array($field, $optional_fields) || isset($value)) {
             // If the input characterset is UTF-8, then convert the string to ISO-8859-1
             // for transfer to SagePay.
             // FIXME: catch invalid UTF-8 stream errors. iconv() can easily fail if you
             // pass it duff data.
             // FIXME: don't make any assumptions about what the input chatset could be. It
             // my not be UTF-8 but maystill need conversion to ISO-8859-1
             if ($this->input_charset == 'UTF-8') {
                 $value = iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $value);
             }
             // Some fields are sent to SagePay as a different name to their local storage.
             // An important one is VendorTxCode, which is used as the primary key of each transaction,
             // but also as a foreign key reference to linked transactions, but has the same name when
             // sent to SagePay in both cases.
             // But only for some transactino service types. Other services use RelatedVendorTxCode as
             // that same field name.
             if ($field == 'RelatedVendorTxCode' && $rename_vendor_tx_code) {
                 $field = 'VendorTxCode';
             }
             $query[$field] = $value;
         }
     }
     if ($format_as_querystring) {
         // Return the query string
         return http_build_query($query, '', '&');
     } else {
         // Just return the data as an array.
         // This may be more useful for some transport packages that like to construct
         // their own query string..
         return $query;
     }
 }
Example #4
0
 /**
  * Generate the database column creation statements from the Transaction metadata.
  * Extend this method to add further custom columns if required.
  */
 public function createColumnsDdl()
 {
     // NOTE: the column lengths listed in the metadata are to support unicode (UTF-8) characters,
     // and not ASCII bytes.
     // If the database is not set up with a unicode charset, then we need to add a percentage
     // to the lengths of all columns.
     $field_meta = Metadata\Transaction::get('object', array('store' => true));
     $columns = array();
     foreach ($field_meta as $name => $field) {
         $column = '`' . $name . '` ';
         if (!isset($field->max)) {
             continue;
         }
         $max = $field->max;
         // If currency, then take the max value as a string and add 4 (for the dp and 3 dp digits)
         // Some currencies have three decimal digits.
         if ($field->type == 'currency') {
             $max = strlen((string) $max) + 4;
         }
         // Now here is a nasty hack.
         // Columns that can accept UTF-8 data need to have their length multiplied by
         // four to (nearly) guarantee a full UTF-8 string can fit in. There are probably ways around
         // this, but the documentation is (and always has been) very confused, mixing up the
         // storage of charactersets, the searching of charactersets and the automatic
         // conversion between the two. This is nasty, nasty, but should work most places
         // in practice.
         if (isset($field->chars) && in_array('^', $field->chars) || $field->type == 'rfc532n' || $field->type == 'html' || $field->type == 'xml') {
             $max = $max * 4;
         }
         // VARCHAR for most columns, apart from the really long ones.
         $datatype = 'VARCHAR';
         if ($max > 2048) {
             $datatype = 'TEXT';
             $max = null;
         }
         $column .= $datatype;
         if (isset($max)) {
             $column .= '(' . $max . ')';
         }
         if (!empty($field->required)) {
             $column .= ' NOT NULL';
         }
         $columns[] = $column;
     }
     return implode(",\n", $columns);
 }
Example #5
0
 /**
  * Return the list of transaction fields to maintain for the transaction.
  * All these fields will be saved to persistent storage.
  */
 public function transactionFields()
 {
     // We only want the fields that will be stored.
     // Actually, we want all fields as we don't know what we will be using. (CHECKME: contradictory)
     $metadata = \Academe\SagePay\Metadata\Transaction::get('array', array('store' => true));
     $result = array();
     foreach ($metadata as $name => $attr) {
         // Get its default value.
         if (isset($attr['default'])) {
             // A default value has been supplied in the metadata.
             $default = $attr['default'];
         } else {
             // Default required fields to an empty string; they will have to be saved.
             $default = !empty($attr['required']) ? '' : null;
         }
         $result[$name] = $default;
     }
     return $result;
 }
Example #6
0
 /**
  * The notification callback for SagePay Server PAYMENT, DEFERRED, AUTHENTICATE.
  * This handles the callback from SagePay in response to a [successful] transaction registration.
  *
  * The redirect URL should not carray any information that allows an end user to be able to
  * highjack it and effect a payment. For this reason, SagePay will be sent to the same page
  * regardless of the status. That page can then inspect the transaction to decide what action
  * to take. A successful payment will be a status of OK, AUTHENTICATED or REGISTERED. A failed
  * payment will be ABORT, NOTAUTHED, REJECTED or ERROR. In the middle is PENDIND, where the
  * transaction is not yet complete (neither paid nor failed) and will take some time to process.
  *
  * Before redirecting to SagePay on registering the transaction, the VendorTxId needs to have been
  * saved to the session. That way the transaction result can be inspected on return to the store,
  * and appropriate action can be taken.
  *
  * @param $post array POST data sent to the page request.
  * @param $redirect_url string The URL SagePay should redirect to, regardless of status.
  */
 public function notification($post, $redirect_url)
 {
     // End of line characters.
     // This is what SagePay expects as a line terminator.
     $eol = "\r\n";
     // Get the main details that identify the transaction.
     $Status = isset($post['Status']) ? (string) $post['Status'] : '';
     $StatusDetail = isset($post['StatusDetail']) ? (string) $post['StatusDetail'] : '';
     $VendorTxCode = isset($post['VendorTxCode']) ? (string) $post['VendorTxCode'] : '';
     $VPSTxId = isset($post['VPSTxId']) ? (string) $post['VPSTxId'] : '';
     // Deal with the quirk noted on page 29 of the token registration docs, i.e. that braces are
     // not returned in the TOKEN notification post. Version 3.00 should have been a good
     // opportunity for SagePay to fix quirks like this, but instead we need to deal with it here.
     if (isset($post['TxType']) && $post['TxType'] == "TOKEN") {
         $VPSTxId = '{' . $VPSTxId . '}';
     }
     $VPSSignature = isset($post['VPSSignature']) ? (string) $post['VPSSignature'] : '';
     // Assume this process will be successful.
     $retStatus = 'OK';
     $retStatusDetail = '';
     // If we have no VendorTxCode then we can go no further.
     if (empty($VendorTxCode)) {
         // Return an appropriate error to the caller.
         $retStatus = 'ERROR';
         $retStatusDetail = 'No VendorTxCode sent';
     }
     if ($retStatus == 'OK') {
         // Get the transaction record.
         // A transaction object should already have been injected to look this up.
         if ($retStatus == 'OK' && $this->getTransactionModel() === null) {
             // Internal error.
             $retStatus = 'INVALID';
             $retStatusDetail = 'Internal error (missing transaction object)';
         } else {
             // Fetch the transaction record from storage.
             $this->findTransaction($VendorTxCode);
         }
     }
     if ($this->getField('VendorTxCode') !== null) {
         if ($this->getField('Status') != 'PENDING') {
             // Already processed status.
             $retStatus = 'INVALID';
             $retStatusDetail = 'Transaction has already been processed';
         } elseif ($VPSTxId != $this->getField('VPSTxId')) {
             // Mis-matching VPSTxId values.
             $retStatus = 'INVALID';
             $retStatusDetail = 'VPSTxId mismatch';
         }
     } else {
         // Return failure to find transaction.
         $retStatus = 'INVALID';
         $retStatusDetail = 'No transaction found';
     }
     // With some of the major checks done, let's dig a little deeper into
     // the transaction to see if it has been tampered with. The anit-tamper
     // checks allows us to used a non-secure connection for the .
     if ($retStatus == 'OK') {
         // Gather some additional parameters, making sure they are all set (defaulting to '').
         // Derive this list from the transaction metadata, with flag "tamper" set.
         $field_meta = Metadata\Transaction::get();
         foreach ($field_meta as $field_name => $field) {
             if (!empty($field->tamper)) {
                 // Make sure a string has been passed in, defaulting to an empty string if necessary.
                 $post[$field_name] = isset($post[$field_name]) ? (string) $post[$field_name] : '';
             }
         }
         /*
             From protocol V3 documentation:
             VPSTxId + VendorTxCode + Status
             + TxAuthNo + VendorName (aka Vendor)
             + AVSCV2 + SecurityKey (saved with the transaction registration)
             + AddressResult + PostCodeResult + CV2Result
             + GiftAid + 3DSecureStatus
             + CAVV + AddressStatus + PayerStatus
             + CardType + Last4Digits
             + DeclineCode + ExpiryDate
             + FraudResponse + BankAuthCode
         */
         // Construct a concatenated POST string hash.
         // These could be constructed from the transaction metadata.
         if (isset($post['TxType']) && $post['TxType'] == "TOKEN") {
             $strMessage = $post['VPSTxId'] . $post['VendorTxCode'] . $post['Status'] . $this->getField('Vendor') . $post['Token'] . $this->getField('SecurityKey');
         } else {
             $strMessage = $post['VPSTxId'] . $post['VendorTxCode'] . $post['Status'] . $post['TxAuthNo'] . $this->getField('Vendor') . $post['AVSCV2'] . $this->getField('SecurityKey') . $post['AddressResult'] . $post['PostCodeResult'] . $post['CV2Result'] . $post['GiftAid'] . $post['3DSecureStatus'] . $post['CAVV'] . $post['AddressStatus'] . $post['PayerStatus'] . $post['CardType'] . $post['Last4Digits'] . $post['DeclineCode'] . $post['ExpiryDate'] . $post['FraudResponse'] . $post['BankAuthCode'];
         }
         $MySignature = strtoupper(md5($strMessage));
         if ($MySignature !== $VPSSignature) {
             // Message that record has been tampered with.
             $retStatus = 'ERROR';
             $retStatusDetail = 'Notification has been tampered with';
         }
     }
     // If still a success, then all tests have passed.
     if ($retStatus == 'OK') {
         // We found a PENDING transaction, so update it.
         // We don't want to be updating the local transaction in any other circumstance.
         // However, we might want to log the errors somewhere else.
         // First SagePay V2 fields.
         $this->setField('Status', $Status);
         $this->setField('StatusDetail', $StatusDetail);
         $this->setField('TxAuthNo', $post['TxAuthNo']);
         $this->setField('AVSCV2', $post['AVSCV2']);
         $this->setField('AddressResult', $post['AddressResult']);
         $this->setField('PostCodeResult', $post['PostCodeResult']);
         $this->setField('CV2Result', $post['CV2Result']);
         $this->setField('GiftAid', $post['GiftAid']);
         $this->setField('3DSecureStatus', $post['3DSecureStatus']);
         $this->setField('CAVV', $post['CAVV']);
         $this->setField('AddressStatus', $post['AddressStatus']);
         $this->setField('PayerStatus', $post['PayerStatus']);
         $this->setField('CardType', $post['CardType']);
         $this->setField('Last4Digits', $post['Last4Digits']);
         // SagePay V3.00 fields.
         // No need to store, or attempt to store, the VPSSignature - it is a throw-away
         // hash of the notification data, with local salt.
         $this->setField('FraudResponse', $post['FraudResponse']);
         $this->setField('Surcharge', $this->arrayElement($post, 'Surcharge'));
         $this->setField('BankAuthCode', $post['BankAuthCode']);
         $this->setField('DeclineCode', $post['DeclineCode']);
         $this->setField('ExpiryDate', $post['ExpiryDate']);
         $this->setField('Token', $this->arrayElement($post, 'Token'));
         // Save the transaction record to local storage.
         // We don't want to throw exceptions here; SagePay must get its response.
         try {
             $this->save();
         } catch (Exception\RuntimeException $e) {
             $retStatus = 'ERROR';
             $retStatusDetail = 'Cannot save result to database: ' . $e->getMessage();
         } catch (RuntimeException $e) {
             $retStatus = 'ERROR';
             $retStatusDetail = 'Cannot save result to database: ' . $e->getMessage();
         }
     }
     // Finally return the result to SagePay, including the relevant redirect URL.
     // If the status sent to us is ERROR, then return INVALID to SagePay.
     // It is not clear why, but the sample code provided by SagePay does this.
     if ($Status == 'ERROR') {
         $retStatus = 'INVALID';
     }
     // Replace any tokens in the URL with values from the transaction, if required.
     // The tokens will be {fieldName} for inserting in a path part of the URL or
     // {{fieldName}} for inserting into a query parameter of the URL.
     // e.g. http://example.com/notification_{Status}.php?id={{VendorTxCode}}
     // although you probably don't want to expose the VendorTxCode.
     $fields = $this->toArray();
     foreach ($fields as $field => $value) {
         $token_path = '{' . $field . '}';
         $token_query = '{' . $token_path . '}';
         // Query parameters and path parts use different escaping.
         if (strpos($redirect_url, $token_query) !== false) {
             $redirect_url = str_replace($token_query, urlencode($value), $redirect_url);
         }
         if (strpos($redirect_url, $token_path) !== false) {
             $redirect_url = str_replace($token_path, rawurlencode($value), $redirect_url);
         }
     }
     // The return string should be fed out to the caller as the only result.
     // The status we send back is one of OK, INVALID or ERROR.
     return 'Status=' . $retStatus . $eol . 'StatusDetail=' . $retStatusDetail . $eol . 'RedirectURL=' . $redirect_url . $eol;
 }
Example #7
0
 public function setUp()
 {
     $this->transactionData = Transaction::get('array');
 }