1<?php 2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project 3// 4// All Rights Reserved. See copyright.txt for details and a complete list of authors. 5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. 6// $Id$ 7 8class CCLiteLib extends TikiDb_Bridge 9{ 10 // member vars (defaults from prefs) 11 private $gateway; 12 private $key_hash; 13 private $registries; 14 private $currencies; 15 private $merchant_user; 16 17 function __construct() 18 { 19 global $prefs; 20 $access = TikiLib::lib('access'); 21 $access->check_feature('payment_feature'); 22 23 // need to add a check for empty, not just y/n - TODO one day 24 //$access->check_feature('payment_cclite_registries'); 25 //$access->check_feature('payment_cclite_gateway'); 26 //$access->check_feature('payment_cclite_merchant_key'); 27 28 $this->gateway = rtrim($prefs['payment_cclite_gateway'], '/'); 29 $this->registries = unserialize($prefs['payment_cclite_registries']); 30 $this->currencies = unserialize($prefs['payment_cclite_currencies']); 31 $this->merchant_user = $prefs['payment_cclite_merchant_user']; 32 33 if (($prefs['payment_cclite_mode'] == 'test' && $_SERVER['SERVER_ADDR'] != '127.0.0.1' && $_SERVER['SERVER_ADDR'] != '::1') || 34 empty($prefs['payment_cclite_test_ip']) 35 ) { 36 $ip = $_SERVER['SERVER_ADDR']; 37 } else { 38 // debug SERVER_ADDR for local testing on NAT'ed server 39 $ip = $prefs['payment_cclite_test_ip']; 40 } 41 $api_hash = hash($prefs['payment_cclite_hashing_algorithm'], ($prefs['payment_cclite_merchant_key'] . $ip), 'true'); 42 $this->key_hash = CCLiteLib::urlsafe_b64encode($api_hash); 43 } 44 45 public function get_registries() 46 { 47 return $this->registries; 48 } 49 50 public function get_registry() 51 { 52 if (! empty($this->registries)) { 53 return $this->registries[0]; // default if not specified in plugins etc 54 } else { 55 Feedback::error(tra('No registries specified in admin/payment/cclite.')); 56 } 57 } 58 59 public function get_currencies() 60 { 61 return $this->currencies; 62 } 63 64 /** 65 * @param string $reg Registry to find currency for (uses registries[0] if not specified) 66 */ 67 public function get_currency($reg = '') 68 { 69 global $prefs; 70 71 if (empty($reg)) { 72 $reg = $this->get_registry(); 73 } 74 75 $i = array_search($reg, $this->registries); 76 77 if ($i !== false) { 78 return $this->currencies[$i]; 79 } else { 80 return $prefs['payment_currency']; 81 } 82 } 83 84 public function get_invoice($ipn_data) 85 { 86 return isset($ipn_data['invoice']) ? $ipn_data['invoice'] : 0; 87 } 88 89 public function get_amount($ipn_data) 90 { 91 return $ipn_data['mc_gross']; 92 } 93 94 public function is_valid($ipn_data, $payment_info) 95 { 96 global $prefs; 97 98 if (! is_array($payment_info)) { 99 return false; 100 } 101 102 // Skip other events 103 if ($ipn_data['payment_status'] != 'Completed') { 104 return false; 105 } 106 107 // Make sure it is addressed to the right account 108 if ($ipn_data['receiver_email'] != $prefs['payment_cclite_business']) { 109 return false; 110 } 111 112 // Require same currency 113 if ($ipn_data['mc_currency'] != $payment_info['currency']) { 114 return false; 115 } 116 117 // Skip duplicate translactions 118 foreach ($payment_info['payments'] as $payment) { 119 if ($payment['type'] == 'cclite') { 120 if ($payment['details']['txn_id'] == $ipn_data['txn_id']) { 121 return false; 122 } 123 } 124 } 125 126 return true; 127 } 128 129 /** 130 * This function just calls $paymentlib->enter_payment() which then triggers the behaviours 131 * The behaviours the do the actual transfer of currency 132 * 133 * @param int $invoice 134 * @param decimal $amount 135 * @param string $currency 136 * @param string $registry 137 * @param string $source_user 138 * 139 * @return string result from cclite 140 */ 141 public function pay_invoice($invoice, $amount, $currency = '', $registry = '', $source_user = '') 142 { 143 global $user, $prefs; 144 $tikilib = TikiLib::lib('tiki'); 145 $paymentlib = TikiLib::lib('payment'); 146 147 $msg = tr('Cclite payment initiated on %0', $tikilib->get_short_datetime($tikilib->now)); 148 149 $paymentlib->enter_payment($invoice, $amount, 'cclite', ['info' => $msg]); 150 151 return $msg; 152 } 153 154 /** 155 * Pays $amount from logged in (or source_user TODO) user to manager account 156 * 157 * @param int $invoice 158 * @param decimal $amount 159 * @param string $currency 160 * @param string $registry 161 * @param string $destination_user 162 * 163 * @return string result from cclite 164 */ 165 public function pay_user($amount, $currency = '', $registry = '', $destination_user = '', $source_user = '') 166 { 167 global $user, $prefs; 168 $paymentlib = TikiLib::lib('payment'); 169 if (empty($source_user)) { 170 $source_user = $this->merchant_user; 171 } 172 173 $res = $this->cclite_send_request('pay', $destination_user, $registry, $amount, $currency, $source_user); 174 175 $r = $this->cclite_send_request('logoff'); 176 177 return $res; 178 } 179 180 /** 181 * Adapted from cclite 0.7 drupal gateway (example) 182 * 183 * @command recent|summary|pay|adduser|modifyuser|debit 184 * @other_user destination for payment - uses merchant_user if empty 185 * @registry cclite registry 186 * @amount amount (decimal/float for cost, or email for adduser command) 187 * @currency currency (same as currency "name" in cclite (not "code" yet) 188 * defaults to registry currency 189 * @main_user source of payment - uses logged-in user if empty 190 * 191 * @return result from cclite server (html hopefully) 192 */ 193 194 function cclite_send_request($command, $other_user = '', $registry = '', $amount = 0, $currency = '', $main_user = '') 195 { 196 global $user, $prefs; 197 198 if (empty($other_user)) { 199 $other_user = $this->merchant_user; 200 } 201 202 if (empty($main_user)) { 203 $main_user = $user; 204 } 205 206 if (empty($registry)) { 207 $registry = $this->get_registry(); 208 } 209 210 if (empty($currency)) { 211 $currency = $this->get_currency($registry); 212 } 213 214 $result = ''; 215 216 // construct the payment url from configuration information 217 $cclite_base_url = $this->gateway; 218 $REST_url = ''; 219 $ch = curl_init(); 220 if ($command != 'adduser') { 221 $logon_result = $this->cclite_remote_logon($main_user, $registry); 222 if ($logon_result[0] != 'failed' && strlen($logon_result[1])) { 223 curl_setopt($ch, CURLOPT_COOKIE, $logon_result[1]); 224 } else { 225 return tr('Connection to Cclite server %0 failed for %1<br />"%2"', $cclite_base_url, $main_user, $logon_result[1]); 226 } 227 } 228 curl_setopt($ch, CURLOPT_AUTOREFERER, true); 229 //curl_setopt($ch, CURLOPT_COOKIESESSION, true); 230 curl_setopt($ch, CURLOPT_FAILONERROR, false); 231 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 232 curl_setopt($ch, CURLOPT_FRESH_CONNECT, false); 233 curl_setopt($ch, CURLOPT_HEADER, false); 234 curl_setopt($ch, CURLOPT_POST, true); 235 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 236 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); 237 238 //curl_setopt($ch, CURLOPT_VERBOSE, true); 239 240 // this switch statement needs to map to the Rewrites in the cclite .htaccess file, so if you're 241 // doing something custom-made, you need to think about: 242 // -here-, .htaccess and various bits of login in the cclite motor 243 switch ($command) { 244 case 'recent': 245 $REST_url = "$cclite_base_url/recent/transactions"; 246 break; 247 248 case 'summary': 249 $REST_url = "$cclite_base_url/summary"; 250 break; 251 252 case 'pay': 253 $REST_url = "$cclite_base_url/pay/$other_user/$registry/$amount/$currency"; 254 break; 255 256 case 'adduser': 257 $REST_url = "$cclite_base_url/direct/adduser/$registry/" . urlencode($other_user . '/' . $amount); 258 curl_setopt($ch, CURLOPT_COOKIE, 'merchant_key_hash=' . $this->key_hash); 259 break; 260 261 case 'modifyuser': 262 // non-working at present... 263 $REST_url = "$cclite_base_url/direct/modifyuser/$registry/" . urlencode($other_user . '/' . $amount); 264 curl_setopt($ch, CURLOPT_COOKIE, 'merchant_key_hash=' . $this->key_hash); 265 break; 266 267 case 'debit': 268 // non-working at present... 269 $REST_url = "$cclite_base_url/debit/$other_user/$registry/$amount/$currency"; 270 break; 271 272 case 'logoff': 273 // non-working at present... 274 $REST_url = "$cclite_base_url/logoff"; 275 break; 276 277 default: 278 return "No cclite function selected use <a title=\"cclite passthrough help\" href=\"/$cclite_base_url/help\">help</a>" ; 279 } 280 281 curl_setopt($ch, CURLOPT_URL, $REST_url); 282 $result = curl_exec($ch); 283 curl_close($ch); 284 return strip_tags($result); 285 } 286 287 /** 288 * Modified from cclite 0.7 gateway various examples 289 * 290 * @return multitype:mixed string |multitype:string 291 */ 292 private function cclite_remote_logon($username = '', $registry = '') 293 { 294 global $user, $prefs; 295 $userlib = TikiLib::lib('user'); 296 297 if (empty($username)) { 298 $username = $user; 299 } 300 301 // not worth trying if no user name 302 if (! empty($username)) { 303 if (empty($registry)) { 304 $registry = $this->get_registry(); 305 } 306 $cclite_base_url = $this->gateway; 307 308 // payment url from configuration information 309 $REST_url = "$cclite_base_url/logon/$username/$registry"; // /$api_hash 310 311 $ch = curl_init(); 312 curl_setopt($ch, CURLOPT_AUTOREFERER, true); 313 curl_setopt($ch, CURLOPT_COOKIE, 'merchant_key_hash=' . $this->key_hash); 314 curl_setopt($ch, CURLOPT_COOKIESESSION, true); 315 curl_setopt($ch, CURLOPT_FAILONERROR, true); 316 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); 317 curl_setopt($ch, CURLOPT_FRESH_CONNECT, true); 318 curl_setopt($ch, CURLOPT_HEADER, true); 319 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 320 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); 321 curl_setopt($ch, CURLOPT_URL, $REST_url); 322 323// curl_setopt($ch, CURLOPT_VERBOSE, true); 324 325 $logon = curl_exec($ch); 326 curl_close($ch); 327 328 $results = []; // for response & cookies on success 329 $err_msg = ''; // error message on failure 330 331 if ($logon) { 332 // e.g. login failed for jonny_tiki at c2c1: <a href="http://c2c.ourproject.org/cgi-bin/cclite.cgi">Try again?</a> 333 if (preg_match('/^(login failed for ' . $username . '.*' . $registry . '[^<]*)/mi', $logon, $results)) { // no user there? 334 $email = $userlib->get_user_email($username); 335 if ($email) { // required 336 $res = $this->cclite_send_request('adduser', $username, $registry, $email); // not currently working cclite 0.7.0 337 if ($res && ! preg_match('/404 Not Found/', $res)) { // seems to return a 404 :( 338 $logon = curl_exec($ch); // retry login 339 } else { 340 $err_msg = trim($results[0]); 341 $logon = 'failed'; 342 } 343 } 344 } 345 346 // e.g. test_user at test_reg is not active: confirm or contact the administrator <a href="http://c2c.ourproject.org/cgi-bin/cclite.cgi">Try again?</a> 347 // check for other errors & remove cclite link 348 if (preg_match('/^(.*?' . $username . '.*' . $registry . '[^<]*)/mi', $logon, $results)) { 349 $err_msg = trim($results[0]); 350 $logon = 'failed'; // error in $results[0] 351 } elseif (preg_match('/HTTP\/1.1 302/mis', $logon) && preg_match('/<BODY.*?>(.*)<\/BODY>/mis', $logon, $results)) { 352 $err_msg = trim(strip_tags($results[0], '<br />')); 353 //$logon = 'failed'; 354 } 355 } 356 357 if ($logon && $logon != 'failed') { 358 preg_match_all('|Set-Cookie: (.*);|U', $logon, $results); 359 $cookies = implode("; ", $results[1]); 360 return [$logon, $cookies]; 361 } 362 } else { 363 $err_msg = 'No result from cclite server.'; 364 } 365 366 return ['failed', $err_msg]; 367 } 368 369 // used to transport merchant key hash - probably duplicates of tiki fns REFACTOR? 370 static function urlsafe_b64encode($string) 371 { 372 $data = base64_encode($string); 373 $data = str_replace(['+', '/', '='], ['-', '_', ''], $data); 374 return $data; 375 } 376 377 static function urlsafe_b64decode($string) 378 { 379 $data = str_replace(['-', '_'], ['+', '/'], $string); 380 $mod4 = strlen($data) % 4; 381 if ($mod4) { 382 $data .= substr('====', $mod4); 383 } 384 return base64_decode($data); 385 } 386} 387 388global $cclitelib; 389$cclitelib = new CCLiteLib; 390