| 1 | <?php |
| 2 | /* |
| 3 | +--------------------------------------------------------------------+ |
| 4 | | CiviCRM version 4.3 | |
| 5 | +--------------------------------------------------------------------+ |
| 6 | | Copyright CiviCRM LLC (c) 2004-2013 | |
| 7 | +--------------------------------------------------------------------+ |
| 8 | | This file is a part of CiviCRM. | |
| 9 | | | |
| 10 | | CiviCRM is free software; you can copy, modify, and distribute it | |
| 11 | | under the terms of the GNU Affero General Public License | |
| 12 | | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | |
| 13 | | | |
| 14 | | CiviCRM is distributed in the hope that it will be useful, but | |
| 15 | | WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 16 | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
| 17 | | See the GNU Affero General Public License for more details. | |
| 18 | | | |
| 19 | | You should have received a copy of the GNU Affero General Public | |
| 20 | | License and the CiviCRM Licensing Exception along | |
| 21 | | with this program; if not, contact CiviCRM LLC | |
| 22 | | at info[AT]civicrm[DOT]org. If you have questions about the | |
| 23 | | GNU Affero General Public License or the licensing of CiviCRM, | |
| 24 | | see the CiviCRM license FAQ at http://civicrm.org/licensing | |
| 25 | +--------------------------------------------------------------------+ |
| 26 | */ |
| 27 | |
| 28 | /** |
| 29 | * |
| 30 | * @package CRM |
| 31 | * @copyright CiviCRM LLC (c) 2004-2013 |
| 32 | * $Id$ |
| 33 | * |
| 34 | */ |
| 35 | |
| 36 | abstract class CRM_Core_Payment { |
| 37 | |
| 38 | /** |
| 39 | * how are we getting billing information? |
| 40 | * |
| 41 | * FORM - we collect it on the same page |
| 42 | * BUTTON - the processor collects it and sends it back to us via some protocol |
| 43 | */ |
| 44 | CONST |
| 45 | BILLING_MODE_FORM = 1, |
| 46 | BILLING_MODE_BUTTON = 2, |
| 47 | BILLING_MODE_NOTIFY = 4; |
| 48 | |
| 49 | /** |
| 50 | * which payment type(s) are we using? |
| 51 | * |
| 52 | * credit card |
| 53 | * direct debit |
| 54 | * or both |
| 55 | * |
| 56 | */ |
| 57 | CONST |
| 58 | PAYMENT_TYPE_CREDIT_CARD = 1, |
| 59 | PAYMENT_TYPE_DIRECT_DEBIT = 2; |
| 60 | |
| 61 | /** |
| 62 | * Subscription / Recurring payment Status |
| 63 | * START, END |
| 64 | * |
| 65 | */ |
| 66 | CONST |
| 67 | RECURRING_PAYMENT_START = 'START', |
| 68 | RECURRING_PAYMENT_END = 'END'; |
| 69 | |
| 70 | /** |
| 71 | * We only need one instance of this object. So we use the singleton |
| 72 | * pattern and cache the instance in this variable |
| 73 | * |
| 74 | * @var object |
| 75 | * @static |
| 76 | */ |
| 77 | static private $_singleton = NULL; |
| 78 | |
| 79 | protected $_paymentProcessor; |
| 80 | |
| 81 | protected $_paymentForm = NULL; |
| 82 | |
| 83 | /** |
| 84 | * singleton function used to manage this object |
| 85 | * |
| 86 | * @param string $mode the mode of operation: live or test |
| 87 | * @param object $paymentProcessor the details of the payment processor being invoked |
| 88 | * @param object $paymentForm reference to the form object if available |
| 89 | * @param boolean $force should we force a reload of this payment object |
| 90 | * |
| 91 | * @return object |
| 92 | * @static |
| 93 | * |
| 94 | */ |
| 95 | static function &singleton($mode = 'test', &$paymentProcessor, &$paymentForm = NULL, $force = FALSE) { |
| 96 | // make sure paymentProcessor is not empty |
| 97 | // CRM-7424 |
| 98 | if (empty($paymentProcessor)) { |
| 99 | return CRM_Core_DAO::$_nullObject; |
| 100 | } |
| 101 | |
| 102 | $cacheKey = "{$mode}_{$paymentProcessor['id']}_" . (int)isset($paymentForm); |
| 103 | if (!isset(self::$_singleton[$cacheKey]) || $force) { |
| 104 | $config = CRM_Core_Config::singleton(); |
| 105 | $ext = CRM_Extension_System::singleton()->getMapper(); |
| 106 | if ($ext->isExtensionKey($paymentProcessor['class_name'])) { |
| 107 | $paymentClass = $ext->keyToClass($paymentProcessor['class_name'], 'payment'); |
| 108 | require_once ($ext->classToPath($paymentClass)); |
| 109 | } |
| 110 | else { |
| 111 | $paymentClass = 'CRM_Core_' . $paymentProcessor['class_name']; |
| 112 | require_once (str_replace('_', DIRECTORY_SEPARATOR, $paymentClass) . '.php'); |
| 113 | } |
| 114 | |
| 115 | //load the object. |
| 116 | self::$_singleton[$cacheKey] = $paymentClass::singleton($mode, $paymentProcessor); |
| 117 | } |
| 118 | |
| 119 | //load the payment form for required processor. |
| 120 | if ($paymentForm !== NULL) { |
| 121 | self::$_singleton[$cacheKey]->setForm($paymentForm); |
| 122 | } |
| 123 | |
| 124 | return self::$_singleton[$cacheKey]; |
| 125 | } |
| 126 | |
| 127 | /** |
| 128 | * Setter for the payment form that wants to use the processor |
| 129 | * |
| 130 | * @param obj $paymentForm |
| 131 | * |
| 132 | */ |
| 133 | function setForm(&$paymentForm) { |
| 134 | $this->_paymentForm = $paymentForm; |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * Getter for payment form that is using the processor |
| 139 | * |
| 140 | * @return obj A form object |
| 141 | */ |
| 142 | function getForm() { |
| 143 | return $this->_paymentForm; |
| 144 | } |
| 145 | |
| 146 | /** |
| 147 | * Getter for accessing member vars |
| 148 | * |
| 149 | */ |
| 150 | function getVar($name) { |
| 151 | return isset($this->$name) ? $this->$name : NULL; |
| 152 | } |
| 153 | |
| 154 | /** |
| 155 | * This function collects all the information from a web/api form and invokes |
| 156 | * the relevant payment processor specific functions to perform the transaction |
| 157 | * |
| 158 | * @param array $params assoc array of input parameters for this transaction |
| 159 | * |
| 160 | * @return array the result in an nice formatted array (or an error object) |
| 161 | * @abstract |
| 162 | */ |
| 163 | abstract function doDirectPayment(&$params); |
| 164 | |
| 165 | /** |
| 166 | * This function checks to see if we have the right config values |
| 167 | * |
| 168 | * @param string $mode the mode we are operating in (live or test) |
| 169 | * |
| 170 | * @return string the error message if any |
| 171 | * @public |
| 172 | */ |
| 173 | abstract function checkConfig(); |
| 174 | |
| 175 | static function paypalRedirect(&$paymentProcessor) { |
| 176 | if (!$paymentProcessor) { |
| 177 | return FALSE; |
| 178 | } |
| 179 | |
| 180 | if (isset($_GET['payment_date']) && |
| 181 | isset($_GET['merchant_return_link']) && |
| 182 | CRM_Utils_Array::value('payment_status', $_GET) == 'Completed' && |
| 183 | $paymentProcessor['payment_processor_type'] == "PayPal_Standard" |
| 184 | ) { |
| 185 | return TRUE; |
| 186 | } |
| 187 | |
| 188 | return FALSE; |
| 189 | } |
| 190 | |
| 191 | /** |
| 192 | * Page callback for civicrm/payment/ipn |
| 193 | * @public |
| 194 | */ |
| 195 | static function handleIPN() { |
| 196 | self::handlePaymentMethod( |
| 197 | 'PaymentNotification', |
| 198 | array( |
| 199 | 'processor_name' => @$_GET['processor_name'], |
| 200 | 'processor_id' => @$_GET['processor_id'], |
| 201 | 'mode' => @$_GET['mode'], |
| 202 | ) |
| 203 | ); |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Payment callback handler |
| 208 | * Load requested payment processor and call that processor's handle<$method> method |
| 209 | * |
| 210 | * @public |
| 211 | */ |
| 212 | static function handlePaymentMethod($method, $params = array( )) { |
| 213 | if (!isset($params['processor_id']) && !isset($params['processor_name'])) { |
| 214 | CRM_Core_Error::fatal("Either 'processor_id' or 'processor_name' param is required for payment callback"); |
| 215 | } |
| 216 | |
| 217 | // Query db for processor .. |
| 218 | $mode = @$params['mode']; |
| 219 | |
| 220 | $sql = "SELECT ppt.class_name, ppt.name as processor_name, pp.id AS processor_id |
| 221 | FROM civicrm_payment_processor_type ppt |
| 222 | INNER JOIN civicrm_payment_processor pp |
| 223 | ON pp.payment_processor_type_id = ppt.id |
| 224 | AND pp.is_active |
| 225 | AND pp.is_test = %1"; |
| 226 | $args[1] = array($mode == 'test' ? 1 : 0, 'Integer'); |
| 227 | |
| 228 | if (isset($params['processor_id'])) { |
| 229 | $sql .= " WHERE pp.id = %2"; |
| 230 | $args[2] = array($params['processor_id'], 'Integer'); |
| 231 | $notfound = "No active instances of payment processor ID#'{$params['processor_id']}' were found."; |
| 232 | } |
| 233 | else { |
| 234 | $sql .= " WHERE ppt.name = %2"; |
| 235 | $args[2] = array($params['processor_name'], 'String'); |
| 236 | $notfound = "No active instances of the '{$params['processor_name']}' payment processor were found."; |
| 237 | } |
| 238 | |
| 239 | $dao = CRM_Core_DAO::executeQuery($sql, $args); |
| 240 | |
| 241 | // Check whether we found anything at all .. |
| 242 | if (!$dao->N) { |
| 243 | CRM_Core_Error::fatal($notfound); |
| 244 | } |
| 245 | |
| 246 | $method = 'handle' . $method; |
| 247 | $extension_instance_found = FALSE; |
| 248 | |
| 249 | // In all likelihood, we'll just end up with the one instance returned here. But it's |
| 250 | // possible we may get more. Hence, iterate through all instances .. |
| 251 | |
| 252 | while ($dao->fetch()) { |
| 253 | // Check pp is extension |
| 254 | $ext = CRM_Extension_System::singleton()->getMapper(); |
| 255 | if ($ext->isExtensionKey($dao->class_name)) { |
| 256 | $extension_instance_found = TRUE; |
| 257 | $paymentClass = $ext->keyToClass($dao->class_name, 'payment'); |
| 258 | require_once $ext->classToPath($paymentClass); |
| 259 | } |
| 260 | else { |
| 261 | // Legacy instance - but there may also be an extension instance, so |
| 262 | // continue on to the next instance and check that one. We'll raise an |
| 263 | // error later on if none are found. |
| 264 | continue; |
| 265 | } |
| 266 | |
| 267 | $paymentProcessor = CRM_Financial_BAO_PaymentProcessor::getPayment($dao->processor_id, $mode); |
| 268 | |
| 269 | // Should never be empty - we already established this processor_id exists and is active. |
| 270 | if (empty($paymentProcessor)) { |
| 271 | continue; |
| 272 | } |
| 273 | |
| 274 | // Instantiate PP |
| 275 | $processorInstance = $paymentClass::singleton($mode, $paymentProcessor); |
| 276 | |
| 277 | // Does PP implement this method, and can we call it? |
| 278 | if (!method_exists($processorInstance, $method) || |
| 279 | !is_callable(array($processorInstance, $method)) |
| 280 | ) { |
| 281 | // No? This will be the case in all instances, so let's just die now |
| 282 | // and not prolong the agony. |
| 283 | CRM_Core_Error::fatal("Payment processor does not implement a '$method' method"); |
| 284 | } |
| 285 | |
| 286 | // Everything, it seems, is ok - execute pp callback handler |
| 287 | $processorInstance->$method(); |
| 288 | } |
| 289 | |
| 290 | if (!$extension_instance_found) CRM_Core_Error::fatal( |
| 291 | "No extension instances of the '{$params['processor_name']}' payment processor were found.<br />" . |
| 292 | "$method method is unsupported in legacy payment processors." |
| 293 | ); |
| 294 | |
| 295 | // Exit here on web requests, allowing just the plain text response to be echoed |
| 296 | if ($method == 'handlePaymentNotification') { |
| 297 | CRM_Utils_System::civiExit(); |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | /** |
| 302 | * Function to check whether a method is present ( & supported ) by the payment processor object. |
| 303 | * |
| 304 | * @param string $method method to check for. |
| 305 | * |
| 306 | * @return boolean |
| 307 | * @public |
| 308 | */ |
| 309 | function isSupported($method = 'cancelSubscription') { |
| 310 | return method_exists(CRM_Utils_System::getClassName($this), $method); |
| 311 | } |
| 312 | |
| 313 | function subscriptionURL($entityID = NULL, $entity = NULL, $action = 'cancel') { |
| 314 | if ($action == 'cancel') { |
| 315 | $url = 'civicrm/contribute/unsubscribe'; |
| 316 | } |
| 317 | elseif ($action == 'billing') { |
| 318 | //in notify mode don't return the update billing url |
| 319 | if ($this->_paymentProcessor['billing_mode'] == self::BILLING_MODE_NOTIFY) { |
| 320 | return NULL; |
| 321 | } |
| 322 | $url = 'civicrm/contribute/updatebilling'; |
| 323 | } |
| 324 | elseif ($action == 'update') { |
| 325 | $url = 'civicrm/contribute/updaterecur'; |
| 326 | } |
| 327 | $session = CRM_Core_Session::singleton(); |
| 328 | $userId = $session->get('userID'); |
| 329 | $checksumValue = ""; |
| 330 | |
| 331 | if ($entityID && $entity == 'membership') { |
| 332 | if (!$userId) { |
| 333 | $contactID = CRM_Core_DAO::getFieldValue("CRM_Member_DAO_Membership", $entityID, "contact_id"); |
| 334 | $checksumValue = CRM_Contact_BAO_Contact_Utils::generateChecksum($contactID, NULL, 'inf'); |
| 335 | $checksumValue = "&cs={$checksumValue}"; |
| 336 | } |
| 337 | return CRM_Utils_System::url($url, "reset=1&mid={$entityID}{$checksumValue}", TRUE, NULL, FALSE, TRUE); |
| 338 | } |
| 339 | |
| 340 | if ($entityID && $entity == 'contribution') { |
| 341 | if (!$userId) { |
| 342 | $contactID = CRM_Core_DAO::getFieldValue("CRM_Contribute_DAO_Contribution", $entityID, "contact_id"); |
| 343 | $checksumValue = CRM_Contact_BAO_Contact_Utils::generateChecksum($contactID, NULL, 'inf'); |
| 344 | $checksumValue = "&cs={$checksumValue}"; |
| 345 | } |
| 346 | return CRM_Utils_System::url($url, "reset=1&coid={$entityID}{$checksumValue}", TRUE, NULL, FALSE, TRUE); |
| 347 | } |
| 348 | |
| 349 | if ($entityID && $entity == 'recur') { |
| 350 | if (!$userId) { |
| 351 | $sql = " |
| 352 | SELECT con.contact_id |
| 353 | FROM civicrm_contribution_recur rec |
| 354 | INNER JOIN civicrm_contribution con ON ( con.contribution_recur_id = rec.id ) |
| 355 | WHERE rec.id = %1 |
| 356 | GROUP BY rec.id"; |
| 357 | $contactID = CRM_Core_DAO::singleValueQuery($sql, array(1 => array($entityID, 'Integer'))); |
| 358 | $checksumValue = CRM_Contact_BAO_Contact_Utils::generateChecksum($contactID, NULL, 'inf'); |
| 359 | $checksumValue = "&cs={$checksumValue}"; |
| 360 | } |
| 361 | return CRM_Utils_System::url($url, "reset=1&crid={$entityID}{$checksumValue}", TRUE, NULL, FALSE, TRUE); |
| 362 | } |
| 363 | |
| 364 | if ($this->isSupported('accountLoginURL')) { |
| 365 | return $this->accountLoginURL(); |
| 366 | } |
| 367 | return $this->_paymentProcessor['url_recur']; |
| 368 | } |
| 369 | |
| 370 | /** |
| 371 | * Check for presence of type 1 or type 3 enabled processors (means we can do back-office submit credit/debit card trxns) |
| 372 | * @public |
| 373 | */ |
| 374 | static function allowBackofficeCreditCard($template = NULL, $variableName = 'newCredit') { |
| 375 | $newCredit = FALSE; |
| 376 | $processors = CRM_Core_PseudoConstant::paymentProcessor(FALSE, FALSE, |
| 377 | "billing_mode IN ( 1, 3 )" |
| 378 | ); |
| 379 | if (count($processors) > 0) { |
| 380 | $newCredit = TRUE; |
| 381 | } |
| 382 | if ($template) { |
| 383 | $template->assign($variableName, $newCredit); |
| 384 | } |
| 385 | return $newCredit; |
| 386 | } |
| 387 | |
| 388 | } |