├── sample-application ├── ui │ ├── images │ │ └── ipay.png │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── ipay.html │ ├── footer.html │ ├── thanks.html │ ├── checkout.html │ ├── header.html │ └── choose.html ├── 3-checkout-using-ipay.PNG ├── 1-choose-products-to-buy.PNG ├── 2-fill-in-buyer-information.PNG ├── config.ini ├── .htaccess ├── lib │ ├── f3.php │ ├── code.css │ ├── log.php │ ├── web │ │ ├── google │ │ │ ├── recaptcha.php │ │ │ └── staticmap.php │ │ ├── geo.php │ │ ├── oauth2.php │ │ ├── pingback.php │ │ └── openid.php │ ├── test.php │ ├── bcrypt.php │ ├── matrix.php │ ├── magic.php │ ├── db │ │ ├── mongo.php │ │ ├── jig.php │ │ ├── jig │ │ │ ├── session.php │ │ │ └── mapper.php │ │ ├── mongo │ │ │ ├── session.php │ │ │ └── mapper.php │ │ ├── sql │ │ │ └── session.php │ │ ├── cursor.php │ │ └── sql.php │ ├── session.php │ ├── utf.php │ ├── audit.php │ ├── basket.php │ ├── auth.php │ ├── IPay.php │ ├── template.php │ ├── smtp.php │ ├── cli │ │ └── ws.php │ └── markdown.php ├── README.md └── index.php ├── README.md └── lib └── IPay.php /sample-application/ui/images/ipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-iPay/master/sample-application/ui/images/ipay.png -------------------------------------------------------------------------------- /sample-application/3-checkout-using-ipay.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-iPay/master/sample-application/3-checkout-using-ipay.PNG -------------------------------------------------------------------------------- /sample-application/1-choose-products-to-buy.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-iPay/master/sample-application/1-choose-products-to-buy.PNG -------------------------------------------------------------------------------- /sample-application/2-fill-in-buyer-information.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-iPay/master/sample-application/2-fill-in-buyer-information.PNG -------------------------------------------------------------------------------- /sample-application/ui/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-iPay/master/sample-application/ui/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /sample-application/ui/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-iPay/master/sample-application/ui/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /sample-application/ui/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-iPay/master/sample-application/ui/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /sample-application/ui/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-iPay/master/sample-application/ui/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /sample-application/ui/ipay.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | {{@content | raw}} 5 |
6 |
7 | -------------------------------------------------------------------------------- /sample-application/ui/footer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 | -------------------------------------------------------------------------------- /sample-application/config.ini: -------------------------------------------------------------------------------- 1 | [globals] 2 | DEBUG=3 3 | UI=ui/ 4 | [IPAY] 5 | vendorID=demo 6 | hashkey=demo 7 | call_back=yourCallBackURL 8 | currency=KES 9 | endpoint=sandbox 10 | log=1 11 | mpesa=1 12 | airtel=1 13 | equity=1 14 | mobilebanking=1 15 | debitcard=1 16 | creditcard=1 17 | mkoporahisi=0 18 | saida=0 19 | autopay=0 20 | cst=1 21 | crl=http -------------------------------------------------------------------------------- /sample-application/ui/thanks.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Thank You!

4 |

Be sure to shop with us next time.Your order is now being processed.

5 |
6 |

7 | Continue to homepage 8 |

9 |
10 | -------------------------------------------------------------------------------- /sample-application/.htaccess: -------------------------------------------------------------------------------- 1 | # Enable rewrite engine and route requests to framework 2 | RewriteEngine On 3 | 4 | # Some servers require you to specify the `RewriteBase` directive 5 | # In such cases, it should be the path (relative to the document root) 6 | # containing this .htaccess file 7 | # 8 | # RewriteBase / 9 | 10 | RewriteRule ^(tmp)\/|\.ini$ - [R=404] 11 | 12 | RewriteCond %{REQUEST_FILENAME} !-l 13 | RewriteCond %{REQUEST_FILENAME} !-f 14 | RewriteCond %{REQUEST_FILENAME} !-d 15 | RewriteRule .* index.php [L,QSA] 16 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L] 17 | -------------------------------------------------------------------------------- /sample-application/ui/checkout.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

iPay Buyer Information

5 |
6 |
7 | 8 |
9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /sample-application/ui/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | F3-iPay Demo 12 | 13 | 14 | -------------------------------------------------------------------------------- /sample-application/lib/f3.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | //! Legacy mode enabler 24 | class F3 { 25 | 26 | static 27 | //! Framework instance 28 | $fw; 29 | 30 | /** 31 | * Forward function calls to framework 32 | * @return mixed 33 | * @param $func callback 34 | * @param $args array 35 | **/ 36 | static function __callstatic($func,array $args) { 37 | if (!self::$fw) 38 | self::$fw=Base::instance(); 39 | return call_user_func_array([self::$fw,$func],$args); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /sample-application/lib/code.css: -------------------------------------------------------------------------------- 1 | code{word-wrap:break-word;color:black}.comment,.doc_comment,.ml_comment{color:dimgray;font-style:italic}.variable{color:blueviolet}.const,.constant_encapsed_string,.class_c,.dir,.file,.func_c,.halt_compiler,.line,.method_c,.lnumber,.dnumber{color:crimson}.string,.and_equal,.boolean_and,.boolean_or,.concat_equal,.dec,.div_equal,.inc,.is_equal,.is_greater_or_equal,.is_identical,.is_not_equal,.is_not_identical,.is_smaller_or_equal,.logical_and,.logical_or,.logical_xor,.minus_equal,.mod_equal,.mul_equal,.ns_c,.ns_separator,.or_equal,.plus_equal,.sl,.sl_equal,.sr,.sr_equal,.xor_equal,.start_heredoc,.end_heredoc,.object_operator,.paamayim_nekudotayim{color:black}.abstract,.array,.array_cast,.as,.break,.case,.catch,.class,.clone,.continue,.declare,.default,.do,.echo,.else,.elseif,.empty.enddeclare,.endfor,.endforach,.endif,.endswitch,.endwhile,.eval,.exit,.extends,.final,.for,.foreach,.function,.global,.goto,.if,.implements,.include,.include_once,.instanceof,.interface,.isset,.list,.namespace,.new,.print,.private,.public,.protected,.require,.require_once,.return,.static,.switch,.throw,.try,.unset,.use,.var,.while{color:royalblue}.open_tag,.open_tag_with_echo,.close_tag{color:orange}.ini_section{color:black}.ini_key{color:royalblue}.ini_value{color:crimson}.xml_tag{color:dodgerblue}.xml_attr{color:blueviolet}.xml_data{color:red}.section{color:black}.directive{color:blue}.data{color:dimgray} 2 | -------------------------------------------------------------------------------- /sample-application/lib/log.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | //! Custom logger 24 | class Log { 25 | 26 | protected 27 | //! File name 28 | $file; 29 | 30 | /** 31 | * Write specified text to log file 32 | * @return string 33 | * @param $text string 34 | * @param $format string 35 | **/ 36 | function write($text,$format='r') { 37 | $fw=Base::instance(); 38 | $fw->write( 39 | $this->file, 40 | date($format). 41 | (isset($_SERVER['REMOTE_ADDR'])? 42 | (' ['.$_SERVER['REMOTE_ADDR'].']'):'').' '. 43 | trim($text).PHP_EOL, 44 | TRUE 45 | ); 46 | } 47 | 48 | /** 49 | * Erase log 50 | * @return NULL 51 | **/ 52 | function erase() { 53 | @unlink($this->file); 54 | } 55 | 56 | /** 57 | * Instantiate class 58 | * @param $file string 59 | **/ 60 | function __construct($file) { 61 | $fw=Base::instance(); 62 | if (!is_dir($dir=$fw->LOGS)) 63 | mkdir($dir,Base::MODE,TRUE); 64 | $this->file=$dir.$file; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /sample-application/lib/web/google/recaptcha.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace Web\Google; 24 | 25 | //! Google ReCAPTCHA v2 plug-in 26 | class Recaptcha { 27 | 28 | const 29 | //! API URL 30 | URL_Recaptcha='https://www.google.com/recaptcha/api/siteverify'; 31 | 32 | /** 33 | * Verify reCAPTCHA response 34 | * @param string $secret 35 | * @param string $response 36 | * @return bool 37 | **/ 38 | static function verify($secret,$response=NULL) { 39 | $fw=\Base::instance(); 40 | if (!isset($response)) 41 | $response=$fw->{'POST.g-recaptcha-response'}; 42 | if (!$response) 43 | return FALSE; 44 | $web=\Web::instance(); 45 | $out=$web->request(self::URL_Recaptcha,[ 46 | 'method'=>'POST', 47 | 'content'=>http_build_query([ 48 | 'secret'=>$secret, 49 | 'response'=>$response, 50 | 'remoteip'=>$fw->IP 51 | ]), 52 | ]); 53 | return isset($out['body']) && 54 | ($json=json_decode($out['body'],TRUE)) && 55 | isset($json['success']) && $json['success']; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /sample-application/README.md: -------------------------------------------------------------------------------- 1 | # F3-IPAY Sample Implementation 2 | 3 | To use this sample application update the config.ini parameters with your environment parameters i.e. 4 | 5 | ```ini 6 | [IPAY] 7 | vendorID=demo 8 | hashkey=demo 9 | call_back=yourCallBackURL 10 | currency=KES 11 | endpoint=sandbox 12 | log=1 13 | mpesa=1 14 | airtel=1 15 | equity=1 16 | mobilebanking=1 17 | debitcard=1 18 | creditcard=1 19 | mkoporahisi=0 20 | saida=0 21 | autopay=0 22 | cst=1 23 | crl=http 24 | ``` 25 | 26 | If being placed in a sub folder also update the RewriteBase line of the .htaccess file e.g. if installed in a sub folder called iPay line 8 of the htaccess will be: 27 | 28 | ``` 29 | RewriteBase /iPay 30 | ``` 31 | 32 | The application takes a 3 step- process to checkout: 33 | * Load shopping cart , currently initialized using the basket function of F3; basically this means add all items needed to cart. 34 | * Fill in mandatory buyer information amount is held from the cart so no need to re-fill it here. 35 | * Post the transaction to IPay for checkout 36 | 37 | 38 | To use in production change the endpoint to production; additionally update the keys and call back URL. 39 | 40 | ## Checkout Process 41 | 1. Select and add items to cart 42 | This helps calculate the total amount 43 | ![Select and add Items to cart](https://github.com/alienwithin/F3-iPay/raw/master/sample-application/1-choose-products-to-buy.PNG "IPay Integration in FatFree") 44 | 45 | 2. Fill in Buyer information 46 | 47 | ![Fill in buyer information that is mandatory](https://github.com/alienwithin/F3-iPay/raw/master/sample-application/2-fill-in-buyer-information.PNG "IPay Integration in FatFree") 48 | 49 | 3. Pay using preferred method on iPay 50 | 51 | ![Pay and confirm transaction on IPay](https://github.com/alienwithin/F3-iPay/raw/master/sample-application/3-checkout-using-ipay.PNG "Ipay Integration in FatFree") 52 | -------------------------------------------------------------------------------- /sample-application/lib/web/google/staticmap.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace Web\Google; 24 | 25 | //! Google Static Maps API v2 plug-in 26 | class StaticMap { 27 | 28 | const 29 | //! API URL 30 | URL_Static='http://maps.googleapis.com/maps/api/staticmap'; 31 | 32 | protected 33 | //! Query arguments 34 | $query=array(); 35 | 36 | /** 37 | * Specify API key-value pair via magic call 38 | * @return object 39 | * @param $func string 40 | * @param $args array 41 | **/ 42 | function __call($func,array $args) { 43 | $this->query[]=array($func,$args[0]); 44 | return $this; 45 | } 46 | 47 | /** 48 | * Generate map 49 | * @return string 50 | **/ 51 | function dump() { 52 | $fw=\Base::instance(); 53 | $web=\Web::instance(); 54 | $out=''; 55 | return ($req=$web->request( 56 | self::URL_Static.'?'.array_reduce( 57 | $this->query, 58 | function($out,$item) { 59 | return ($out.=($out?'&':''). 60 | urlencode($item[0]).'='.urlencode($item[1])); 61 | } 62 | ))) && $req['body']?$req['body']:FALSE; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /sample-application/lib/test.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | //! Unit test kit 24 | class Test { 25 | 26 | //@{ Reporting level 27 | const 28 | FLAG_False=0, 29 | FLAG_True=1, 30 | FLAG_Both=2; 31 | //@} 32 | 33 | protected 34 | //! Test results 35 | $data=[], 36 | //! Success indicator 37 | $passed=TRUE; 38 | 39 | /** 40 | * Return test results 41 | * @return array 42 | **/ 43 | function results() { 44 | return $this->data; 45 | } 46 | 47 | /** 48 | * Return FALSE if at least one test case fails 49 | * @return bool 50 | **/ 51 | function passed() { 52 | return $this->passed; 53 | } 54 | 55 | /** 56 | * Evaluate condition and save test result 57 | * @return object 58 | * @param $cond bool 59 | * @param $text string 60 | **/ 61 | function expect($cond,$text=NULL) { 62 | $out=(bool)$cond; 63 | if ($this->level==$out || $this->level==self::FLAG_Both) { 64 | $data=['status'=>$out,'text'=>$text,'source'=>NULL]; 65 | foreach (debug_backtrace() as $frame) 66 | if (isset($frame['file'])) { 67 | $data['source']=Base::instance()-> 68 | fixslashes($frame['file']).':'.$frame['line']; 69 | break; 70 | } 71 | $this->data[]=$data; 72 | } 73 | if (!$out && $this->passed) 74 | $this->passed=FALSE; 75 | return $this; 76 | } 77 | 78 | /** 79 | * Append message to test results 80 | * @return NULL 81 | * @param $text string 82 | **/ 83 | function message($text) { 84 | $this->expect(TRUE,$text); 85 | } 86 | 87 | /** 88 | * Class constructor 89 | * @return NULL 90 | * @param $level int 91 | **/ 92 | function __construct($level=self::FLAG_Both) { 93 | $this->level=$level; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /sample-application/ui/choose.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
Shopping Cart
11 |
12 |
13 |
You currently have {{@itemcount}} item(s) in your cart.
14 |
15 |
16 | 19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 |
28 |
29 |

{{@item.name}}

30 |
31 |
32 |
33 |
{{@item.amount}} x
34 |
35 |
36 |
{{@item.qty}}
37 |
38 |
39 | 42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
Added items?
51 |
52 |
53 | 56 |
57 |
58 |
59 |
60 | 72 |
73 |
74 |
75 |
76 | -------------------------------------------------------------------------------- /sample-application/lib/bcrypt.php: -------------------------------------------------------------------------------- 1 | . 19 | * 20 | **/ 21 | 22 | /** 23 | * Lightweight password hashing library (PHP 5.5+ only) 24 | * @deprecated Use http://php.net/manual/en/ref.password.php instead 25 | **/ 26 | class Bcrypt extends Prefab { 27 | 28 | //@{ Error messages 29 | const 30 | E_CostArg='Invalid cost parameter', 31 | E_SaltArg='Salt must be at least 22 alphanumeric characters'; 32 | //@} 33 | 34 | //! Default cost 35 | const 36 | COST=10; 37 | 38 | /** 39 | * Generate bcrypt hash of string 40 | * @return string|FALSE 41 | * @param $pw string 42 | * @param $salt string 43 | * @param $cost int 44 | **/ 45 | function hash($pw,$salt=NULL,$cost=self::COST) { 46 | if ($cost<4 || $cost>31) 47 | user_error(self::E_CostArg,E_USER_ERROR); 48 | $len=22; 49 | if ($salt) { 50 | if (!preg_match('/^[[:alnum:]\.\/]{'.$len.',}$/',$salt)) 51 | user_error(self::E_SaltArg,E_USER_ERROR); 52 | } 53 | else { 54 | $raw=16; 55 | $iv=''; 56 | if (!$iv && extension_loaded('openssl')) 57 | $iv=openssl_random_pseudo_bytes($raw); 58 | if (!$iv) 59 | for ($i=0;$i<$raw;$i++) 60 | $iv.=chr(mt_rand(0,255)); 61 | $salt=str_replace('+','.',base64_encode($iv)); 62 | } 63 | $salt=substr($salt,0,$len); 64 | $hash=crypt($pw,sprintf('$2y$%02d$',$cost).$salt); 65 | return strlen($hash)>13?$hash:FALSE; 66 | } 67 | 68 | /** 69 | * Check if password is still strong enough 70 | * @return bool 71 | * @param $hash string 72 | * @param $cost int 73 | **/ 74 | function needs_rehash($hash,$cost=self::COST) { 75 | list($pwcost)=sscanf($hash,"$2y$%d$"); 76 | return $pwcost<$cost; 77 | } 78 | 79 | /** 80 | * Verify password against hash using timing attack resistant approach 81 | * @return bool 82 | * @param $pw string 83 | * @param $hash string 84 | **/ 85 | function verify($pw,$hash) { 86 | $val=crypt($pw,$hash); 87 | $len=strlen($val); 88 | if ($len!=strlen($hash) || $len<14) 89 | return FALSE; 90 | $out=0; 91 | for ($i=0;$i<$len;$i++) 92 | $out|=(ord($val[$i])^ord($hash[$i])); 93 | return $out===0; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /sample-application/index.php: -------------------------------------------------------------------------------- 1 | config('config.ini'); 8 | 9 | /*Home page loads the basket and prepopulates Items 10 | Ideally this is where you would have the logic for basket for users to select items and add to cart. 11 | In this case we have just initialized as it is a demo 12 | */ 13 | $f3->route('GET /', 14 | function($f3) { 15 | $basket = new \Basket(); 16 | $basket->drop(); 17 | 18 | // add item 19 | $basket->set('name', 'Kahawa'); 20 | $basket->set('amount', '15.00'); 21 | $basket->set('qty', '2'); 22 | $basket->save(); 23 | $basket->reset(); 24 | // add item 25 | $basket->set('name', 'Mafuta'); 26 | $basket->set('amount', '100.00'); 27 | $basket->set('qty', '1'); 28 | $basket->save(); 29 | $basket->reset(); 30 | 31 | $cart = $basket->find(); 32 | foreach ($cart as $item) { 33 | $subtotal += $item['amount'] * $item['qty']; 34 | $itemcount+=$item['qty']; 35 | } 36 | $f3->set('itemcount', $itemcount); 37 | $f3->set('cartitems', $cart); 38 | $f3->set('subtotal', sprintf("%01.2f", $subtotal)); 39 | echo \Template::instance()->render('choose.html'); 40 | } 41 | ); 42 | /* 43 | Here we display the cart summary as the checkout process begins. 44 | We get the buyer/billing information and number of items in basket 45 | */ 46 | $f3->route('GET|POST /checkout', 47 | function ($f3) { 48 | $basket = new \Basket(); 49 | $f3->set('itemcount', $basket->count()); 50 | echo \Template::instance()->render('checkout.html'); 51 | } 52 | ); 53 | /* 54 | We then perform the checkout finalization by sending the required items to iPay; 55 | */ 56 | $f3->route('GET|POST /ipay', 57 | function ($f3) { 58 | $basket = new \Basket(); 59 | $cartitems = $basket->find(); 60 | $iPay= new IPay; 61 | /*Define iPay Mandatory Variables*/ 62 | $orderID=generateIPayTransactionID(); 63 | $email=$f3->get('POST.email'); 64 | $telephone=$f3->get('POST.telephone'); 65 | $subtotal = $iPay->copyBasket($cartitems); 66 | /*End Define iPay Mandatory Variables*/ 67 | //Generate request and post to iPay 68 | $content=$iPay->process_iPay_payment($orderID,$subtotal,$email,$telephone,$p1="",$p2="",$p3="",$p4=""); 69 | $f3->set('content',$content); 70 | //Render on page 71 | echo \Template::instance()->render('ipay.html'); 72 | } 73 | ); 74 | /*iPay responds to our call back URL*/ 75 | $f3->route('GET|POST /thankyou', 76 | function ($f3) { 77 | //perform DB operations update to PENDING after getting a tracking ID 78 | echo \Template::instance()->render('thanks.html'); 79 | } 80 | ); 81 | 82 | /* 83 | Simple function used to generate alphanumeric transaction IDs 84 | */ 85 | function generateIPayTransactionID($length = 10) { 86 | $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 87 | $charactersLength = strlen($characters); 88 | $randomString = ''; 89 | for ($i = 0; $i < $length; $i++) { 90 | $randomString .= $characters[rand(0, $charactersLength - 1)]; 91 | } 92 | return $randomString; 93 | } 94 | $f3->run(); 95 | -------------------------------------------------------------------------------- /sample-application/lib/matrix.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | //! Generic array utilities 24 | class Matrix extends Prefab { 25 | 26 | /** 27 | * Retrieve values from a specified column of a multi-dimensional 28 | * array variable 29 | * @return array 30 | * @param $var array 31 | * @param $col mixed 32 | **/ 33 | function pick(array $var,$col) { 34 | return array_map( 35 | function($row) use($col) { 36 | return $row[$col]; 37 | }, 38 | $var 39 | ); 40 | } 41 | 42 | /** 43 | * Rotate a two-dimensional array variable 44 | * @return NULL 45 | * @param $var array 46 | **/ 47 | function transpose(array &$var) { 48 | $out=[]; 49 | foreach ($var as $keyx=>$cols) 50 | foreach ($cols as $keyy=>$valy) 51 | $out[$keyy][$keyx]=$valy; 52 | $var=$out; 53 | } 54 | 55 | /** 56 | * Sort a multi-dimensional array variable on a specified column 57 | * @return bool 58 | * @param $var array 59 | * @param $col mixed 60 | * @param $order int 61 | **/ 62 | function sort(array &$var,$col,$order=SORT_ASC) { 63 | uasort( 64 | $var, 65 | function($val1,$val2) use($col,$order) { 66 | list($v1,$v2)=[$val1[$col],$val2[$col]]; 67 | $out=is_numeric($v1) && is_numeric($v2)? 68 | Base::instance()->sign($v1-$v2):strcmp($v1,$v2); 69 | if ($order==SORT_DESC) 70 | $out=-$out; 71 | return $out; 72 | } 73 | ); 74 | $var=array_values($var); 75 | } 76 | 77 | /** 78 | * Change the key of a two-dimensional array element 79 | * @return NULL 80 | * @param $var array 81 | * @param $old string 82 | * @param $new string 83 | **/ 84 | function changekey(array &$var,$old,$new) { 85 | $keys=array_keys($var); 86 | $vals=array_values($var); 87 | $keys[array_search($old,$keys)]=$new; 88 | $var=array_combine($keys,$vals); 89 | } 90 | 91 | /** 92 | * Return month calendar of specified date, with optional setting for 93 | * first day of week (0 for Sunday) 94 | * @return array 95 | * @param $date string 96 | * @param $first int 97 | **/ 98 | function calendar($date='now',$first=0) { 99 | $out=FALSE; 100 | if (extension_loaded('calendar')) { 101 | $parts=getdate(strtotime($date)); 102 | $days=cal_days_in_month(CAL_GREGORIAN,$parts['mon'],$parts['year']); 103 | $ref=date('w',strtotime(date('Y-m',$parts[0]).'-01'))+(7-$first)%7; 104 | $out=[]; 105 | for ($i=0;$i<$days;$i++) 106 | $out[floor(($ref+$i)/7)][($ref+$i)%7]=$i+1; 107 | } 108 | return $out; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /sample-application/lib/web/geo.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace Web; 24 | 25 | //! Geo plug-in 26 | class Geo extends \Prefab { 27 | 28 | /** 29 | * Return information about specified Unix time zone 30 | * @return array 31 | * @param $zone string 32 | **/ 33 | function tzinfo($zone) { 34 | $ref=new \DateTimeZone($zone); 35 | $loc=$ref->getLocation(); 36 | $trn=$ref->getTransitions($now=time(),$now); 37 | $out=[ 38 | 'offset'=>$ref-> 39 | getOffset(new \DateTime('now',new \DateTimeZone('UTC')))/3600, 40 | 'country'=>$loc['country_code'], 41 | 'latitude'=>$loc['latitude'], 42 | 'longitude'=>$loc['longitude'], 43 | 'dst'=>$trn[0]['isdst'] 44 | ]; 45 | unset($ref); 46 | return $out; 47 | } 48 | 49 | /** 50 | * Return geolocation data based on specified/auto-detected IP address 51 | * @return array|FALSE 52 | * @param $ip string 53 | **/ 54 | function location($ip=NULL) { 55 | $fw=\Base::instance(); 56 | $web=\Web::instance(); 57 | if (!$ip) 58 | $ip=$fw->IP; 59 | $public=filter_var($ip,FILTER_VALIDATE_IP, 60 | FILTER_FLAG_IPV4|FILTER_FLAG_IPV6| 61 | FILTER_FLAG_NO_RES_RANGE|FILTER_FLAG_NO_PRIV_RANGE); 62 | if (function_exists('geoip_db_avail') && 63 | geoip_db_avail(GEOIP_CITY_EDITION_REV1) && 64 | $out=@geoip_record_by_name($ip)) { 65 | $out['request']=$ip; 66 | $out['region_code']=$out['region']; 67 | $out['region_name']=geoip_region_name_by_code( 68 | $out['country_code'],$out['region']); 69 | unset($out['country_code3'],$out['region'],$out['postal_code']); 70 | return $out; 71 | } 72 | if (($req=$web->request('http://www.geoplugin.net/json.gp'. 73 | ($public?('?ip='.$ip):''))) && 74 | $data=json_decode($req['body'],TRUE)) { 75 | $out=[]; 76 | foreach ($data as $key=>$val) 77 | if (!strpos($key,'currency') && $key!=='geoplugin_status' 78 | && $key!=='geoplugin_region') 79 | $out[$fw->snakecase(substr($key, 10))]=$val; 80 | return $out; 81 | } 82 | return FALSE; 83 | } 84 | 85 | /** 86 | * Return weather data based on specified latitude/longitude 87 | * @return array|FALSE 88 | * @param $latitude float 89 | * @param $longitude float 90 | * @param $key string 91 | **/ 92 | function weather($latitude,$longitude,$key) { 93 | $fw=\Base::instance(); 94 | $web=\Web::instance(); 95 | $query=[ 96 | 'lat'=>$latitude, 97 | 'lon'=>$longitude, 98 | 'APPID'=>$key, 99 | 'units'=>'metric' 100 | ]; 101 | return ($req=$web->request( 102 | 'http://api.openweathermap.org/data/2.5/weather?'. 103 | http_build_query($query)))? 104 | json_decode($req['body'],TRUE): 105 | FALSE; 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /sample-application/lib/magic.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | //! PHP magic wrapper 24 | abstract class Magic implements ArrayAccess { 25 | 26 | /** 27 | * Return TRUE if key is not empty 28 | * @return bool 29 | * @param $key string 30 | **/ 31 | abstract function exists($key); 32 | 33 | /** 34 | * Bind value to key 35 | * @return mixed 36 | * @param $key string 37 | * @param $val mixed 38 | **/ 39 | abstract function set($key,$val); 40 | 41 | /** 42 | * Retrieve contents of key 43 | * @return mixed 44 | * @param $key string 45 | **/ 46 | abstract function &get($key); 47 | 48 | /** 49 | * Unset key 50 | * @return NULL 51 | * @param $key string 52 | **/ 53 | abstract function clear($key); 54 | 55 | /** 56 | * Convenience method for checking property value 57 | * @return mixed 58 | * @param $key string 59 | **/ 60 | function offsetexists($key) { 61 | return Base::instance()->visible($this,$key)? 62 | isset($this->$key):$this->exists($key); 63 | } 64 | 65 | /** 66 | * Convenience method for assigning property value 67 | * @return mixed 68 | * @param $key string 69 | * @param $val scalar 70 | **/ 71 | function offsetset($key,$val) { 72 | return Base::instance()->visible($this,$key)? 73 | ($this->$key=$val):$this->set($key,$val); 74 | } 75 | 76 | /** 77 | * Convenience method for retrieving property value 78 | * @return mixed 79 | * @param $key string 80 | **/ 81 | function &offsetget($key) { 82 | if (Base::instance()->visible($this,$key)) 83 | $val=&$this->$key; 84 | else 85 | $val=&$this->get($key); 86 | return $val; 87 | } 88 | 89 | /** 90 | * Convenience method for removing property value 91 | * @return NULL 92 | * @param $key string 93 | **/ 94 | function offsetunset($key) { 95 | if (Base::instance()->visible($this,$key)) 96 | unset($this->$key); 97 | else 98 | $this->clear($key); 99 | } 100 | 101 | /** 102 | * Alias for offsetexists() 103 | * @return mixed 104 | * @param $key string 105 | **/ 106 | function __isset($key) { 107 | return $this->offsetexists($key); 108 | } 109 | 110 | /** 111 | * Alias for offsetset() 112 | * @return mixed 113 | * @param $key string 114 | * @param $val scalar 115 | **/ 116 | function __set($key,$val) { 117 | return $this->offsetset($key,$val); 118 | } 119 | 120 | /** 121 | * Alias for offsetget() 122 | * @return mixed 123 | * @param $key string 124 | **/ 125 | function &__get($key) { 126 | $val=&$this->offsetget($key); 127 | return $val; 128 | } 129 | 130 | /** 131 | * Alias for offsetunset() 132 | * @return NULL 133 | * @param $key string 134 | **/ 135 | function __unset($key) { 136 | $this->offsetunset($key); 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /sample-application/lib/web/oauth2.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace Web; 24 | 25 | //! Lightweight OAuth2 client 26 | class OAuth2 extends \Magic { 27 | 28 | protected 29 | //! Scopes and claims 30 | $args=[]; 31 | 32 | /** 33 | * Return OAuth2 authentication URI 34 | * @return string 35 | * @param $endpoint string 36 | **/ 37 | function uri($endpoint) { 38 | return $endpoint.'?'.http_build_query($this->args); 39 | } 40 | 41 | /** 42 | * Send request to API/token endpoint 43 | * @return string|FALSE 44 | * @param $uri string 45 | * @param $method string 46 | * @param $token array 47 | **/ 48 | function request($uri,$method,$token=NULL) { 49 | $web=\Web::instance(); 50 | $options=[ 51 | 'method'=>$method, 52 | 'content'=>http_build_query($this->args), 53 | 'header'=>['Accept: application/json'] 54 | ]; 55 | if ($token) 56 | array_push($options['header'],'Authorization: Bearer '.$token); 57 | elseif ($method=='POST') 58 | array_push($options['header'],'Authorization: Basic '. 59 | base64_encode( 60 | $this->args['client_id'].':'. 61 | $this->args['client_secret'] 62 | ) 63 | ); 64 | $response=$web->request($uri,$options); 65 | if ($response['error']) 66 | user_error($response['error'],E_USER_ERROR); 67 | return $response['body'] && 68 | preg_grep('/HTTP\/1\.\d 200/',$response['headers'])? 69 | json_decode($response['body'],TRUE): 70 | FALSE; 71 | } 72 | 73 | /** 74 | * Parse JSON Web token 75 | * @return array 76 | * @param $token string 77 | **/ 78 | function jwt($token) { 79 | return json_decode( 80 | base64_decode( 81 | str_replace( 82 | ['-','_'], 83 | ['+','/'], 84 | explode('.',$token)[1] 85 | ) 86 | ), 87 | TRUE 88 | ); 89 | } 90 | 91 | /** 92 | * Return TRUE if scope/claim exists 93 | * @return bool 94 | * @param $key string 95 | **/ 96 | function exists($key) { 97 | return isset($this->args[$key]); 98 | } 99 | 100 | /** 101 | * Bind value to scope/claim 102 | * @return string 103 | * @param $key string 104 | * @param $val string 105 | **/ 106 | function set($key,$val) { 107 | return $this->args[$key]=$val; 108 | } 109 | 110 | /** 111 | * Return value of scope/claim 112 | * @return mixed 113 | * @param $key string 114 | **/ 115 | function &get($key) { 116 | if (isset($this->args[$key])) 117 | $val=&$this->args[$key]; 118 | else 119 | $val=NULL; 120 | return $val; 121 | } 122 | 123 | /** 124 | * Remove scope/claim 125 | * @return NULL 126 | * @param $key string 127 | **/ 128 | function clear($key=NULL) { 129 | if ($key) 130 | unset($this->args[$key]); 131 | else 132 | $this->args=[]; 133 | } 134 | 135 | } 136 | 137 | -------------------------------------------------------------------------------- /sample-application/lib/db/mongo.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace DB; 24 | 25 | //! MongoDB wrapper 26 | class Mongo { 27 | 28 | //@{ 29 | const 30 | E_Profiler='MongoDB profiler is disabled'; 31 | //@} 32 | 33 | protected 34 | //! UUID 35 | $uuid, 36 | //! Data source name 37 | $dsn, 38 | //! MongoDB object 39 | $db, 40 | //! Legacy flag 41 | $legacy, 42 | //! MongoDB log 43 | $log; 44 | 45 | /** 46 | * Return data source name 47 | * @return string 48 | **/ 49 | function dsn() { 50 | return $this->dsn; 51 | } 52 | 53 | /** 54 | * Return UUID 55 | * @return string 56 | **/ 57 | function uuid() { 58 | return $this->uuid; 59 | } 60 | 61 | /** 62 | * Return MongoDB profiler results (or disable logging) 63 | * @param $flag bool 64 | * @return string 65 | **/ 66 | function log($flag=TRUE) { 67 | if ($flag) { 68 | $cursor=$this->db->selectcollection('system.profile')->find(); 69 | foreach (iterator_to_array($cursor) as $frame) 70 | if (!preg_match('/\.system\..+$/',$frame['ns'])) 71 | $this->log.=date('r',$frame['ts']->sec).' ('. 72 | sprintf('%.1f',$frame['millis']).'ms) '. 73 | $frame['ns'].' ['.$frame['op'].'] '. 74 | (empty($frame['query'])? 75 | '':json_encode($frame['query'])). 76 | (empty($frame['command'])? 77 | '':json_encode($frame['command'])). 78 | PHP_EOL; 79 | } else { 80 | $this->log=FALSE; 81 | if ($this->legacy) 82 | $this->db->setprofilinglevel(-1); 83 | else 84 | $this->db->command(['profile'=>-1]); 85 | } 86 | return $this->log; 87 | } 88 | 89 | /** 90 | * Intercept native call to re-enable profiler 91 | * @return int 92 | **/ 93 | function drop() { 94 | $out=$this->db->drop(); 95 | if ($this->log!==FALSE) { 96 | if ($this->legacy) 97 | $this->db->setprofilinglevel(2); 98 | else 99 | $this->db->command(['profile'=>2]); 100 | } 101 | return $out; 102 | } 103 | 104 | /** 105 | * Redirect call to MongoDB object 106 | * @return mixed 107 | * @param $func string 108 | * @param $args array 109 | **/ 110 | function __call($func,array $args) { 111 | return call_user_func_array([$this->db,$func],$args); 112 | } 113 | 114 | /** 115 | * Return TRUE if legacy driver is loaded 116 | * @return bool 117 | **/ 118 | function legacy() { 119 | return $this->legacy; 120 | } 121 | 122 | //! Prohibit cloning 123 | private function __clone() { 124 | } 125 | 126 | /** 127 | * Instantiate class 128 | * @param $dsn string 129 | * @param $dbname string 130 | * @param $options array 131 | **/ 132 | function __construct($dsn,$dbname,array $options=NULL) { 133 | $this->uuid=\Base::instance()->hash($this->dsn=$dsn); 134 | if ($this->legacy=class_exists('\MongoClient')) { 135 | $this->db=new \MongoDB(new \MongoClient($dsn,$options?:[]),$dbname); 136 | $this->db->setprofilinglevel(2); 137 | } 138 | else { 139 | $this->db=(new \MongoDB\Client($dsn,$options?:[]))->$dbname; 140 | $this->db->command(['profile'=>2]); 141 | } 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /sample-application/lib/db/jig.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace DB; 24 | 25 | //! In-memory/flat-file DB wrapper 26 | class Jig { 27 | 28 | //@{ Storage formats 29 | const 30 | FORMAT_JSON=0, 31 | FORMAT_Serialized=1; 32 | //@} 33 | 34 | protected 35 | //! UUID 36 | $uuid, 37 | //! Storage location 38 | $dir, 39 | //! Current storage format 40 | $format, 41 | //! Jig log 42 | $log, 43 | //! Memory-held data 44 | $data; 45 | 46 | /** 47 | * Read data from memory/file 48 | * @return array 49 | * @param $file string 50 | **/ 51 | function &read($file) { 52 | if (!$this->dir || !is_file($dst=$this->dir.$file)) { 53 | if (!isset($this->data[$file])) 54 | $this->data[$file]=[]; 55 | return $this->data[$file]; 56 | } 57 | $fw=\Base::instance(); 58 | $raw=$fw->read($dst); 59 | switch ($this->format) { 60 | case self::FORMAT_JSON: 61 | $data=json_decode($raw,TRUE); 62 | break; 63 | case self::FORMAT_Serialized: 64 | $data=$fw->unserialize($raw); 65 | break; 66 | } 67 | $this->data[$file] = $data; 68 | return $this->data[$file]; 69 | } 70 | 71 | /** 72 | * Write data to memory/file 73 | * @return int 74 | * @param $file string 75 | * @param $data array 76 | **/ 77 | function write($file,array $data=NULL) { 78 | if (!$this->dir) 79 | return count($this->data[$file]=$data); 80 | $fw=\Base::instance(); 81 | switch ($this->format) { 82 | case self::FORMAT_JSON: 83 | $out=json_encode($data,JSON_PRETTY_PRINT); 84 | break; 85 | case self::FORMAT_Serialized: 86 | $out=$fw->serialize($data); 87 | break; 88 | } 89 | return $fw->write($this->dir.'/'.$file,$out); 90 | } 91 | 92 | /** 93 | * Return directory 94 | * @return string 95 | **/ 96 | function dir() { 97 | return $this->dir; 98 | } 99 | 100 | /** 101 | * Return UUID 102 | * @return string 103 | **/ 104 | function uuid() { 105 | return $this->uuid; 106 | } 107 | 108 | /** 109 | * Return profiler results (or disable logging) 110 | * @param $flag bool 111 | * @return string 112 | **/ 113 | function log($flag=TRUE) { 114 | if ($flag) 115 | return $this->log; 116 | $this->log=FALSE; 117 | } 118 | 119 | /** 120 | * Jot down log entry 121 | * @return NULL 122 | * @param $frame string 123 | **/ 124 | function jot($frame) { 125 | if ($frame) 126 | $this->log.=date('r').' '.$frame.PHP_EOL; 127 | } 128 | 129 | /** 130 | * Clean storage 131 | * @return NULL 132 | **/ 133 | function drop() { 134 | if (!$this->dir) 135 | $this->data=[]; 136 | elseif ($glob=@glob($this->dir.'/*',GLOB_NOSORT)) 137 | foreach ($glob as $file) 138 | @unlink($file); 139 | } 140 | 141 | //! Prohibit cloning 142 | private function __clone() { 143 | } 144 | 145 | /** 146 | * Instantiate class 147 | * @param $dir string 148 | * @param $format int 149 | **/ 150 | function __construct($dir=NULL,$format=self::FORMAT_JSON) { 151 | if ($dir && !is_dir($dir)) 152 | mkdir($dir,\Base::MODE,TRUE); 153 | $this->uuid=\Base::instance()->hash($this->dir=$dir); 154 | $this->format=$format; 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /sample-application/lib/session.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | //! Cache-based session handler 24 | class Session { 25 | 26 | protected 27 | //! Session ID 28 | $sid, 29 | //! Anti-CSRF token 30 | $_csrf, 31 | //! User agent 32 | $_agent, 33 | //! IP, 34 | $_ip, 35 | //! Suspect callback 36 | $onsuspect, 37 | //! Cache instance 38 | $_cache; 39 | 40 | /** 41 | * Open session 42 | * @return TRUE 43 | * @param $path string 44 | * @param $name string 45 | **/ 46 | function open($path,$name) { 47 | return TRUE; 48 | } 49 | 50 | /** 51 | * Close session 52 | * @return TRUE 53 | **/ 54 | function close() { 55 | $this->sid=NULL; 56 | return TRUE; 57 | } 58 | 59 | /** 60 | * Return session data in serialized format 61 | * @return string 62 | * @param $id string 63 | **/ 64 | function read($id) { 65 | $this->sid=$id; 66 | if (!$data=$this->_cache->get($id.'.@')) 67 | return ''; 68 | if ($data['ip']!=$this->_ip || $data['agent']!=$this->_agent) { 69 | $fw=Base::instance(); 70 | if (!isset($this->onsuspect) || 71 | $fw->call($this->onsuspect,[$this,$id])===FALSE) { 72 | //NB: `session_destroy` can't be called at that stage (`session_start` not completed) 73 | $this->destroy($id); 74 | $this->close(); 75 | unset($fw->{'COOKIE.'.session_name()}); 76 | $fw->error(403); 77 | } 78 | } 79 | return $data['data']; 80 | } 81 | 82 | /** 83 | * Write session data 84 | * @return TRUE 85 | * @param $id string 86 | * @param $data string 87 | **/ 88 | function write($id,$data) { 89 | $fw=Base::instance(); 90 | $jar=$fw->JAR; 91 | $this->_cache->set($id.'.@', 92 | [ 93 | 'data'=>$data, 94 | 'ip'=>$this->_ip, 95 | 'agent'=>$this->_agent, 96 | 'stamp'=>time() 97 | ], 98 | $jar['expire']?($jar['expire']-time()):0 99 | ); 100 | return TRUE; 101 | } 102 | 103 | /** 104 | * Destroy session 105 | * @return TRUE 106 | * @param $id string 107 | **/ 108 | function destroy($id) { 109 | $this->_cache->clear($id.'.@'); 110 | return TRUE; 111 | } 112 | 113 | /** 114 | * Garbage collector 115 | * @return TRUE 116 | * @param $max int 117 | **/ 118 | function cleanup($max) { 119 | $this->_cache->reset('.@',$max); 120 | return TRUE; 121 | } 122 | 123 | /** 124 | * Return session id (if session has started) 125 | * @return string|NULL 126 | **/ 127 | function sid() { 128 | return $this->sid; 129 | } 130 | 131 | /** 132 | * Return anti-CSRF token 133 | * @return string 134 | **/ 135 | function csrf() { 136 | return $this->_csrf; 137 | } 138 | 139 | /** 140 | * Return IP address 141 | * @return string 142 | **/ 143 | function ip() { 144 | return $this->_ip; 145 | } 146 | 147 | /** 148 | * Return Unix timestamp 149 | * @return string|FALSE 150 | **/ 151 | function stamp() { 152 | if (!$this->sid) 153 | session_start(); 154 | return $this->_cache->exists($this->sid.'.@',$data)? 155 | $data['stamp']:FALSE; 156 | } 157 | 158 | /** 159 | * Return HTTP user agent 160 | * @return string 161 | **/ 162 | function agent() { 163 | return $this->_agent; 164 | } 165 | 166 | /** 167 | * Instantiate class 168 | * @param $onsuspect callback 169 | * @param $key string 170 | **/ 171 | function __construct($onsuspect=NULL,$key=NULL,$cache=null) { 172 | $this->onsuspect=$onsuspect; 173 | $this->_cache=$cache?:Cache::instance(); 174 | session_set_save_handler( 175 | [$this,'open'], 176 | [$this,'close'], 177 | [$this,'read'], 178 | [$this,'write'], 179 | [$this,'destroy'], 180 | [$this,'cleanup'] 181 | ); 182 | register_shutdown_function('session_commit'); 183 | $fw=\Base::instance(); 184 | $headers=$fw->HEADERS; 185 | $this->_csrf=$fw->SEED.'.'.$fw->hash(mt_rand()); 186 | if ($key) 187 | $fw->$key=$this->_csrf; 188 | $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; 189 | $this->_ip=$fw->IP; 190 | } 191 | 192 | } 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # F3-iPay 2 | F3-iPay is a Fat Free Framework plugin that helps in easy implementation of iPay web Checkout. 3 | 4 | 5 | ## Quick Start Config 6 | Add the following custom section to your project config if using the ini style configuration. 7 | 8 | ```ini 9 | [IPAY] 10 | vendorID=demo 11 | hashkey=demo 12 | call_back=yourCallBackURL 13 | currency=KES 14 | endpoint=sandbox 15 | log=1 16 | mpesa=1 17 | airtel=1 18 | equity=1 19 | mobilebanking=1 20 | debitcard=1 21 | creditcard=1 22 | mkoporahisi=0 23 | saida=0 24 | autopay=0 25 | cst=1 26 | crl=http 27 | ``` 28 | 29 | - vendorID - Vendor ID assigned by iPay if on test use demo 30 | - hashkey - hashkey assigned by iPay if on test use demo 31 | - call_back - The URL that iPay redirects your buyers to after they have Checked out 32 | - currency - The currency in use e.g. KES or USD 33 | - endpoint - API Endpoint, values can be 'sandbox' or 'production' which hydrates the parameter to determine if app is live or not. 34 | - log - logs all API requests & responses to iPay.log 35 | - mpesa - Display Mpesa Mobile Money Channel (on or off). “on” by Default (i.e. mpesa=1) 36 | - airtel - Display Airtel Mobile Money Channel (on or off). “on” by Default (i.e. airtel=1) 37 | - equity - Display the Equity EazzyPay Channel (on or off). “on” by Default (i.e. equity=1) 38 | - mobilebanking - Display the Mobile Banking Channel (on or off). “off” by Default (i.e. mobilebanking=0) 39 | - debitcard - Display the Debit Card Channel (on or off). “off” by Default (i.e. debitcard=0) 40 | - creditcard - Display the Credit Card Channel (on or off). “off” by Default (i.e. creditcard=1) 41 | - mkoporahisi - Display Mkopo Rahisi Channel (on or off). “off” by Default(i.e. mkoporahisi=0) 42 | - saida - Display Saida Channel (on or off). “off” by Default (i.e. saida=0) 43 | - autopay - Push Data (on or off). “off” by Default (i.e. autopay=0) Set this parameter to 1 if you want iPay to silently trigger the callback. The CONFIRM button on the checkout page will not be present for mobile money and mobile banking NOTE Valid Callback Parameter must be provided. when this parameter is set iPay will send data to your server using GET request and the IPN should be run to verify this data. 44 | - cst - The customer email notification flag of value 1 or 0. (Set to “1” By Default to allow customer to receive txn notifications from iPay for online txns) 45 | crl - Name of the cURL flag input field (1 character). 46 | crl=http for http/https call back 47 | crl=data_stream for a data stream of comma separated values 48 | crl=json for a json data stream. 49 | (Set to “http” By Default) 50 | 51 | If you prefer you can also pass an array with above values when you instantiate the classes. 52 | 53 | ```php 54 | // F3-iPay config 55 | $iPayConfig = array( 56 | 'vendorID'=>'demo', 57 | 'hashkey'=>'demo', 58 | 'call_back'=>'yourCallBackURL', 59 | 'currency'=>'KES', 60 | 'endpoint'=>'sandbox', 61 | 'log'=>'1', 62 | 'mpesa'=>'1', 63 | 'airtel'=>'1', 64 | 'equity'=>'1', 65 | 'mobilebanking'=>'1', 66 | 'debitcard'=>'1', 67 | 'creditcard'=>'1', 68 | 'mkoporahisi'=>'0', 69 | 'saida'=>'0', 70 | 'autopay'=>'0', 71 | 'cst'=>'1', 72 | 'crl'=>'http' 73 | ); 74 | 75 | // Instantiate the class with config 76 | $iPay=new iPay($iPayConfig); 77 | ``` 78 | 79 | 80 | **Manual Install** 81 | Copy the `lib/IPay.php` file into your `lib/` or your AUTOLOAD folder. 82 | 83 | 84 | 85 | ## Quick Start 86 | ### iPay Checkout 87 | The process is going to be a multistep process as below assuming a simple form based test: 88 | 1. Create IPay Instance 89 | 90 | ```php 91 | $iPay=new IPay; 92 | ``` 93 | 2. Populate iPay mandatory variables to create the valid XML to post to iPay 94 | ```php 95 | /*Define IPay Mandatory Variables*/ 96 | 97 | $email=$f3->get('POST.email'); 98 | $telephone=$f3->get('POST.telephone'); 99 | $totalAmount = $f3->get('POST.Amount'); 100 | /*End Define IPay Mandatory Variables*/ 101 | //Create IPay Checkout URL and submit 102 | //You can do some DB operations here based on the variables as you POST the XML 103 | $content=$iPay->process_iPay_payment($orderID,$totalAmount,$email,$telephone,$p1="",$p2="",$p3="",$p4=""); 104 | //define area to load the generated Ipaycheckout form for submission using the F3 Hive 105 | $f3->set('content',$content); 106 | //Render on page 107 | echo \Template::instance()->render('ipay.html'); 108 | ``` 109 | 110 | Your actual View i.e. ipay.html will be tagged a sample is as below: 111 | ```html 112 | 113 | {{@content | raw}} 114 | 115 | ``` 116 | 117 | View the [sample application](https://github.com/alienwithin/F3-iPay/tree/master/sample-application) to understand implementation aspects of the gateway and test it. 118 | 119 | ## License 120 | F3-IPay is licensed under GPL v.3 121 | -------------------------------------------------------------------------------- /sample-application/lib/db/jig/session.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace DB\Jig; 24 | 25 | //! Jig-managed session handler 26 | class Session extends Mapper { 27 | 28 | protected 29 | //! Session ID 30 | $sid, 31 | //! Anti-CSRF token 32 | $_csrf, 33 | //! User agent 34 | $_agent, 35 | //! IP, 36 | $_ip, 37 | //! Suspect callback 38 | $onsuspect; 39 | 40 | /** 41 | * Open session 42 | * @return TRUE 43 | * @param $path string 44 | * @param $name string 45 | **/ 46 | function open($path,$name) { 47 | return TRUE; 48 | } 49 | 50 | /** 51 | * Close session 52 | * @return TRUE 53 | **/ 54 | function close() { 55 | $this->reset(); 56 | $this->sid=NULL; 57 | return TRUE; 58 | } 59 | 60 | /** 61 | * Return session data in serialized format 62 | * @return string 63 | * @param $id string 64 | **/ 65 | function read($id) { 66 | $this->load(['@session_id=?',$this->sid=$id]); 67 | if ($this->dry()) 68 | return ''; 69 | if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { 70 | $fw=\Base::instance(); 71 | if (!isset($this->onsuspect) || 72 | $fw->call($this->onsuspect,[$this,$id])===FALSE) { 73 | // NB: `session_destroy` can't be called at that stage; 74 | // `session_start` not completed 75 | $this->destroy($id); 76 | $this->close(); 77 | unset($fw->{'COOKIE.'.session_name()}); 78 | $fw->error(403); 79 | } 80 | } 81 | return $this->get('data'); 82 | } 83 | 84 | /** 85 | * Write session data 86 | * @return TRUE 87 | * @param $id string 88 | * @param $data string 89 | **/ 90 | function write($id,$data) { 91 | $this->set('session_id',$id); 92 | $this->set('data',$data); 93 | $this->set('ip',$this->_ip); 94 | $this->set('agent',$this->_agent); 95 | $this->set('stamp',time()); 96 | $this->save(); 97 | return TRUE; 98 | } 99 | 100 | /** 101 | * Destroy session 102 | * @return TRUE 103 | * @param $id string 104 | **/ 105 | function destroy($id) { 106 | $this->erase(['@session_id=?',$id]); 107 | return TRUE; 108 | } 109 | 110 | /** 111 | * Garbage collector 112 | * @return TRUE 113 | * @param $max int 114 | **/ 115 | function cleanup($max) { 116 | $this->erase(['@stamp+?sid; 126 | } 127 | 128 | /** 129 | * Return anti-CSRF token 130 | * @return string 131 | **/ 132 | function csrf() { 133 | return $this->_csrf; 134 | } 135 | 136 | /** 137 | * Return IP address 138 | * @return string 139 | **/ 140 | function ip() { 141 | return $this->_ip; 142 | } 143 | 144 | /** 145 | * Return Unix timestamp 146 | * @return string|FALSE 147 | **/ 148 | function stamp() { 149 | if (!$this->sid) 150 | session_start(); 151 | return $this->dry()?FALSE:$this->get('stamp'); 152 | } 153 | 154 | /** 155 | * Return HTTP user agent 156 | * @return string|FALSE 157 | **/ 158 | function agent() { 159 | return $this->_agent; 160 | } 161 | 162 | /** 163 | * Instantiate class 164 | * @param $db \DB\Jig 165 | * @param $file string 166 | * @param $onsuspect callback 167 | * @param $key string 168 | **/ 169 | function __construct(\DB\Jig $db,$file='sessions',$onsuspect=NULL,$key=NULL) { 170 | parent::__construct($db,$file); 171 | $this->onsuspect=$onsuspect; 172 | session_set_save_handler( 173 | [$this,'open'], 174 | [$this,'close'], 175 | [$this,'read'], 176 | [$this,'write'], 177 | [$this,'destroy'], 178 | [$this,'cleanup'] 179 | ); 180 | register_shutdown_function('session_commit'); 181 | $fw=\Base::instance(); 182 | $headers=$fw->HEADERS; 183 | $this->_csrf=$fw->SEED.'.'.$fw->hash(mt_rand()); 184 | if ($key) 185 | $fw->$key=$this->_csrf; 186 | $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; 187 | $this->_ip=$fw->IP; 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /sample-application/lib/db/mongo/session.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace DB\Mongo; 24 | 25 | //! MongoDB-managed session handler 26 | class Session extends Mapper { 27 | 28 | protected 29 | //! Session ID 30 | $sid, 31 | //! Anti-CSRF token 32 | $_csrf, 33 | //! User agent 34 | $_agent, 35 | //! IP, 36 | $_ip, 37 | //! Suspect callback 38 | $onsuspect; 39 | 40 | /** 41 | * Open session 42 | * @return TRUE 43 | * @param $path string 44 | * @param $name string 45 | **/ 46 | function open($path,$name) { 47 | return TRUE; 48 | } 49 | 50 | /** 51 | * Close session 52 | * @return TRUE 53 | **/ 54 | function close() { 55 | $this->reset(); 56 | $this->sid=NULL; 57 | return TRUE; 58 | } 59 | 60 | /** 61 | * Return session data in serialized format 62 | * @return string 63 | * @param $id string 64 | **/ 65 | function read($id) { 66 | $this->load(['session_id'=>$this->sid=$id]); 67 | if ($this->dry()) 68 | return ''; 69 | if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { 70 | $fw=\Base::instance(); 71 | if (!isset($this->onsuspect) || 72 | $fw->call($this->onsuspect,[$this,$id])===FALSE) { 73 | // NB: `session_destroy` can't be called at that stage; 74 | // `session_start` not completed 75 | $this->destroy($id); 76 | $this->close(); 77 | unset($fw->{'COOKIE.'.session_name()}); 78 | $fw->error(403); 79 | } 80 | } 81 | return $this->get('data'); 82 | } 83 | 84 | /** 85 | * Write session data 86 | * @return TRUE 87 | * @param $id string 88 | * @param $data string 89 | **/ 90 | function write($id,$data) { 91 | $this->set('session_id',$id); 92 | $this->set('data',$data); 93 | $this->set('ip',$this->_ip); 94 | $this->set('agent',$this->_agent); 95 | $this->set('stamp',time()); 96 | $this->save(); 97 | return TRUE; 98 | } 99 | 100 | /** 101 | * Destroy session 102 | * @return TRUE 103 | * @param $id string 104 | **/ 105 | function destroy($id) { 106 | $this->erase(['session_id'=>$id]); 107 | return TRUE; 108 | } 109 | 110 | /** 111 | * Garbage collector 112 | * @return TRUE 113 | * @param $max int 114 | **/ 115 | function cleanup($max) { 116 | $this->erase(['$where'=>'this.stamp+'.$max.'<'.time()]); 117 | return TRUE; 118 | } 119 | 120 | /** 121 | * Return session id (if session has started) 122 | * @return string|NULL 123 | **/ 124 | function sid() { 125 | return $this->sid; 126 | } 127 | 128 | /** 129 | * Return anti-CSRF token 130 | * @return string 131 | **/ 132 | function csrf() { 133 | return $this->_csrf; 134 | } 135 | 136 | /** 137 | * Return IP address 138 | * @return string 139 | **/ 140 | function ip() { 141 | return $this->_ip; 142 | } 143 | 144 | /** 145 | * Return Unix timestamp 146 | * @return string|FALSE 147 | **/ 148 | function stamp() { 149 | if (!$this->sid) 150 | session_start(); 151 | return $this->dry()?FALSE:$this->get('stamp'); 152 | } 153 | 154 | /** 155 | * Return HTTP user agent 156 | * @return string 157 | **/ 158 | function agent() { 159 | return $this->_agent; 160 | } 161 | 162 | /** 163 | * Instantiate class 164 | * @param $db \DB\Mongo 165 | * @param $table string 166 | * @param $onsuspect callback 167 | * @param $key string 168 | **/ 169 | function __construct(\DB\Mongo $db,$table='sessions',$onsuspect=NULL,$key=NULL) { 170 | parent::__construct($db,$table); 171 | $this->onsuspect=$onsuspect; 172 | session_set_save_handler( 173 | [$this,'open'], 174 | [$this,'close'], 175 | [$this,'read'], 176 | [$this,'write'], 177 | [$this,'destroy'], 178 | [$this,'cleanup'] 179 | ); 180 | register_shutdown_function('session_commit'); 181 | $fw=\Base::instance(); 182 | $headers=$fw->HEADERS; 183 | $this->_csrf=$fw->SEED.'.'.$fw->hash(mt_rand()); 184 | if ($key) 185 | $fw->$key=$this->_csrf; 186 | $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; 187 | $this->_ip=$fw->IP; 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /sample-application/lib/db/sql/session.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace DB\SQL; 24 | 25 | //! SQL-managed session handler 26 | class Session extends Mapper { 27 | 28 | protected 29 | //! Session ID 30 | $sid, 31 | //! Anti-CSRF token 32 | $_csrf, 33 | //! User agent 34 | $_agent, 35 | //! IP, 36 | $_ip, 37 | //! Suspect callback 38 | $onsuspect; 39 | 40 | /** 41 | * Open session 42 | * @return TRUE 43 | * @param $path string 44 | * @param $name string 45 | **/ 46 | function open($path,$name) { 47 | return TRUE; 48 | } 49 | 50 | /** 51 | * Close session 52 | * @return TRUE 53 | **/ 54 | function close() { 55 | $this->reset(); 56 | $this->sid=NULL; 57 | return TRUE; 58 | } 59 | 60 | /** 61 | * Return session data in serialized format 62 | * @return string 63 | * @param $id string 64 | **/ 65 | function read($id) { 66 | $this->load(['session_id=?',$this->sid=$id]); 67 | if ($this->dry()) 68 | return ''; 69 | if ($this->get('ip')!=$this->_ip || $this->get('agent')!=$this->_agent) { 70 | $fw=\Base::instance(); 71 | if (!isset($this->onsuspect) || 72 | $fw->call($this->onsuspect,[$this,$id])===FALSE) { 73 | //NB: `session_destroy` can't be called at that stage (`session_start` not completed) 74 | $this->destroy($id); 75 | $this->close(); 76 | unset($fw->{'COOKIE.'.session_name()}); 77 | $fw->error(403); 78 | } 79 | } 80 | return $this->get('data'); 81 | } 82 | 83 | /** 84 | * Write session data 85 | * @return TRUE 86 | * @param $id string 87 | * @param $data string 88 | **/ 89 | function write($id,$data) { 90 | $this->set('session_id',$id); 91 | $this->set('data',$data); 92 | $this->set('ip',$this->_ip); 93 | $this->set('agent',$this->_agent); 94 | $this->set('stamp',time()); 95 | $this->save(); 96 | return TRUE; 97 | } 98 | 99 | /** 100 | * Destroy session 101 | * @return TRUE 102 | * @param $id string 103 | **/ 104 | function destroy($id) { 105 | $this->erase(['session_id=?',$id]); 106 | return TRUE; 107 | } 108 | 109 | /** 110 | * Garbage collector 111 | * @return TRUE 112 | * @param $max int 113 | **/ 114 | function cleanup($max) { 115 | $this->erase(['stamp+?sid; 125 | } 126 | 127 | /** 128 | * Return anti-CSRF token 129 | * @return string 130 | **/ 131 | function csrf() { 132 | return $this->_csrf; 133 | } 134 | 135 | /** 136 | * Return IP address 137 | * @return string 138 | **/ 139 | function ip() { 140 | return $this->_ip; 141 | } 142 | 143 | /** 144 | * Return Unix timestamp 145 | * @return string|FALSE 146 | **/ 147 | function stamp() { 148 | if (!$this->sid) 149 | session_start(); 150 | return $this->dry()?FALSE:$this->get('stamp'); 151 | } 152 | 153 | /** 154 | * Return HTTP user agent 155 | * @return string 156 | **/ 157 | function agent() { 158 | return $this->_agent; 159 | } 160 | 161 | /** 162 | * Instantiate class 163 | * @param $db \DB\SQL 164 | * @param $table string 165 | * @param $force bool 166 | * @param $onsuspect callback 167 | * @param $key string 168 | **/ 169 | function __construct(\DB\SQL $db,$table='sessions',$force=TRUE,$onsuspect=NULL,$key=NULL) { 170 | if ($force) { 171 | $eol="\n"; 172 | $tab="\t"; 173 | $db->exec( 174 | (preg_match('/mssql|sqlsrv|sybase/',$db->driver())? 175 | ('IF NOT EXISTS (SELECT * FROM sysobjects WHERE '. 176 | 'name='.$db->quote($table).' AND xtype=\'U\') '. 177 | 'CREATE TABLE dbo.'): 178 | ('CREATE TABLE IF NOT EXISTS '. 179 | ((($name=$db->name())&&$db->driver()!='pgsql')? 180 | ($db->quotekey($name,FALSE).'.'):''))). 181 | $db->quotekey($table,FALSE).' ('.$eol. 182 | $tab.$db->quotekey('session_id').' VARCHAR(255),'.$eol. 183 | $tab.$db->quotekey('data').' TEXT,'.$eol. 184 | $tab.$db->quotekey('ip').' VARCHAR(45),'.$eol. 185 | $tab.$db->quotekey('agent').' VARCHAR(300),'.$eol. 186 | $tab.$db->quotekey('stamp').' INTEGER,'.$eol. 187 | $tab.'PRIMARY KEY ('.$db->quotekey('session_id').')'.$eol. 188 | ');' 189 | ); 190 | } 191 | parent::__construct($db,$table); 192 | $this->onsuspect=$onsuspect; 193 | session_set_save_handler( 194 | [$this,'open'], 195 | [$this,'close'], 196 | [$this,'read'], 197 | [$this,'write'], 198 | [$this,'destroy'], 199 | [$this,'cleanup'] 200 | ); 201 | register_shutdown_function('session_commit'); 202 | $fw=\Base::instance(); 203 | $headers=$fw->HEADERS; 204 | $this->_csrf=$fw->SEED.'.'.$fw->hash(mt_rand()); 205 | if ($key) 206 | $fw->$key=$this->_csrf; 207 | $this->_agent=isset($headers['User-Agent'])?$headers['User-Agent']:''; 208 | $this->_ip=$fw->IP; 209 | } 210 | 211 | } 212 | -------------------------------------------------------------------------------- /sample-application/lib/utf.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | //! Unicode string manager 24 | class UTF extends Prefab { 25 | 26 | /** 27 | * Get string length 28 | * @return int 29 | * @param $str string 30 | **/ 31 | function strlen($str) { 32 | preg_match_all('/./us',$str,$parts); 33 | return count($parts[0]); 34 | } 35 | 36 | /** 37 | * Reverse a string 38 | * @return string 39 | * @param $str string 40 | **/ 41 | function strrev($str) { 42 | preg_match_all('/./us',$str,$parts); 43 | return implode('',array_reverse($parts[0])); 44 | } 45 | 46 | /** 47 | * Find position of first occurrence of a string (case-insensitive) 48 | * @return int|FALSE 49 | * @param $stack string 50 | * @param $needle string 51 | * @param $ofs int 52 | **/ 53 | function stripos($stack,$needle,$ofs=0) { 54 | return $this->strpos($stack,$needle,$ofs,TRUE); 55 | } 56 | 57 | /** 58 | * Find position of first occurrence of a string 59 | * @return int|FALSE 60 | * @param $stack string 61 | * @param $needle string 62 | * @param $ofs int 63 | * @param $case bool 64 | **/ 65 | function strpos($stack,$needle,$ofs=0,$case=FALSE) { 66 | return preg_match('/^(.{'.$ofs.'}.*?)'. 67 | preg_quote($needle,'/').'/us'.($case?'i':''),$stack,$match)? 68 | $this->strlen($match[1]):FALSE; 69 | } 70 | 71 | /** 72 | * Returns part of haystack string from the first occurrence of 73 | * needle to the end of haystack (case-insensitive) 74 | * @return string|FALSE 75 | * @param $stack string 76 | * @param $needle string 77 | * @param $before bool 78 | **/ 79 | function stristr($stack,$needle,$before=FALSE) { 80 | return $this->strstr($stack,$needle,$before,TRUE); 81 | } 82 | 83 | /** 84 | * Returns part of haystack string from the first occurrence of 85 | * needle to the end of haystack 86 | * @return string|FALSE 87 | * @param $stack string 88 | * @param $needle string 89 | * @param $before bool 90 | * @param $case bool 91 | **/ 92 | function strstr($stack,$needle,$before=FALSE,$case=FALSE) { 93 | if (!$needle) 94 | return FALSE; 95 | preg_match('/^(.*?)'.preg_quote($needle,'/').'/us'.($case?'i':''), 96 | $stack,$match); 97 | return isset($match[1])? 98 | ($before? 99 | $match[1]: 100 | $this->substr($stack,$this->strlen($match[1]))): 101 | FALSE; 102 | } 103 | 104 | /** 105 | * Return part of a string 106 | * @return string|FALSE 107 | * @param $str string 108 | * @param $start int 109 | * @param $len int 110 | **/ 111 | function substr($str,$start,$len=0) { 112 | if ($start<0) 113 | $start=$this->strlen($str)+$start; 114 | if (!$len) 115 | $len=$this->strlen($str)-$start; 116 | return preg_match('/^.{'.$start.'}(.{0,'.$len.'})/us',$str,$match)? 117 | $match[1]:FALSE; 118 | } 119 | 120 | /** 121 | * Count the number of substring occurrences 122 | * @return int 123 | * @param $stack string 124 | * @param $needle string 125 | **/ 126 | function substr_count($stack,$needle) { 127 | preg_match_all('/'.preg_quote($needle,'/').'/us',$stack, 128 | $matches,PREG_SET_ORDER); 129 | return count($matches); 130 | } 131 | 132 | /** 133 | * Strip whitespaces from the beginning of a string 134 | * @return string 135 | * @param $str string 136 | **/ 137 | function ltrim($str) { 138 | return preg_replace('/^[\pZ\pC]+/u','',$str); 139 | } 140 | 141 | /** 142 | * Strip whitespaces from the end of a string 143 | * @return string 144 | * @param $str string 145 | **/ 146 | function rtrim($str) { 147 | return preg_replace('/[\pZ\pC]+$/u','',$str); 148 | } 149 | 150 | /** 151 | * Strip whitespaces from the beginning and end of a string 152 | * @return string 153 | * @param $str string 154 | **/ 155 | function trim($str) { 156 | return preg_replace('/^[\pZ\pC]+|[\pZ\pC]+$/u','',$str); 157 | } 158 | 159 | /** 160 | * Return UTF-8 byte order mark 161 | * @return string 162 | **/ 163 | function bom() { 164 | return chr(0xef).chr(0xbb).chr(0xbf); 165 | } 166 | 167 | /** 168 | * Convert code points to Unicode symbols 169 | * @return string 170 | * @param $str string 171 | **/ 172 | function translate($str) { 173 | return html_entity_decode( 174 | preg_replace('/\\\\u([[:xdigit:]]+)/i','&#x\1;',$str)); 175 | } 176 | 177 | /** 178 | * Translate emoji tokens to Unicode font-supported symbols 179 | * @return string 180 | * @param $str string 181 | **/ 182 | function emojify($str) { 183 | $map=[ 184 | ':('=>'\u2639', // frown 185 | ':)'=>'\u263a', // smile 186 | '<3'=>'\u2665', // heart 187 | ':D'=>'\u1f603', // grin 188 | 'XD'=>'\u1f606', // laugh 189 | ';)'=>'\u1f609', // wink 190 | ':P'=>'\u1f60b', // tongue 191 | ':,'=>'\u1f60f', // think 192 | ':/'=>'\u1f623', // skeptic 193 | '8O'=>'\u1f632', // oops 194 | ]+Base::instance()->EMOJI; 195 | return $this->translate(str_replace(array_keys($map), 196 | array_values($map),$str)); 197 | } 198 | 199 | } 200 | -------------------------------------------------------------------------------- /sample-application/lib/web/pingback.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace Web; 24 | 25 | //! Pingback 1.0 protocol (client and server) implementation 26 | class Pingback extends \Prefab { 27 | 28 | protected 29 | //! Transaction history 30 | $log; 31 | 32 | /** 33 | * Return TRUE if URL points to a pingback-enabled resource 34 | * @return bool 35 | * @param $url 36 | **/ 37 | protected function enabled($url) { 38 | $web=\Web::instance(); 39 | $req=$web->request($url); 40 | $found=FALSE; 41 | if ($req['body']) { 42 | // Look for pingback header 43 | foreach ($req['headers'] as $header) 44 | if (preg_match('/^X-Pingback:\h*(.+)/',$header,$href)) { 45 | $found=$href[1]; 46 | break; 47 | } 48 | if (!$found && 49 | // Scan page for pingback link tag 50 | preg_match('//i',$req['body'],$parts) && 51 | preg_match('/rel\h*=\h*"pingback"/i',$parts[1]) && 52 | preg_match('/href\h*=\h*"\h*(.+?)\h*"/i',$parts[1],$href)) 53 | $found=$href[1]; 54 | } 55 | return $found; 56 | } 57 | 58 | /** 59 | * Load local page contents, parse HTML anchor tags, find permalinks, 60 | * and send XML-RPC calls to corresponding pingback servers 61 | * @return NULL 62 | * @param $source string 63 | **/ 64 | function inspect($source) { 65 | $fw=\Base::instance(); 66 | $web=\Web::instance(); 67 | $parts=parse_url($source); 68 | if (empty($parts['scheme']) || empty($parts['host']) || 69 | $parts['host']==$fw->HOST) { 70 | $req=$web->request($source); 71 | $doc=new \DOMDocument('1.0',$fw->ENCODING); 72 | $doc->stricterrorchecking=FALSE; 73 | $doc->recover=TRUE; 74 | if (@$doc->loadhtml($req['body'])) { 75 | // Parse anchor tags 76 | $links=$doc->getelementsbytagname('a'); 77 | foreach ($links as $link) { 78 | $permalink=$link->getattribute('href'); 79 | // Find pingback-enabled resources 80 | if ($permalink && $found=$this->enabled($permalink)) { 81 | $req=$web->request($found, 82 | [ 83 | 'method'=>'POST', 84 | 'header'=>'Content-Type: application/xml', 85 | 'content'=>xmlrpc_encode_request( 86 | 'pingback.ping', 87 | [$source,$permalink], 88 | ['encoding'=>$fw->ENCODING] 89 | ) 90 | ] 91 | ); 92 | if ($req['body']) 93 | $this->log.=date('r').' '. 94 | $permalink.' [permalink:'.$found.']'.PHP_EOL. 95 | $req['body'].PHP_EOL; 96 | } 97 | } 98 | } 99 | unset($doc); 100 | } 101 | } 102 | 103 | /** 104 | * Receive ping, check if local page is pingback-enabled, verify 105 | * source contents, and return XML-RPC response 106 | * @return string 107 | * @param $func callback 108 | * @param $path string 109 | **/ 110 | function listen($func,$path=NULL) { 111 | $fw=\Base::instance(); 112 | if (PHP_SAPI!='cli') { 113 | header('X-Powered-By: '.$fw->PACKAGE); 114 | header('Content-Type: application/xml; '. 115 | 'charset='.$charset=$fw->ENCODING); 116 | } 117 | if (!$path) 118 | $path=$fw->BASE; 119 | $web=\Web::instance(); 120 | $args=xmlrpc_decode_request($fw->BODY,$method,$charset); 121 | $options=['encoding'=>$charset]; 122 | if ($method=='pingback.ping' && isset($args[0],$args[1])) { 123 | list($source,$permalink)=$args; 124 | $doc=new \DOMDocument('1.0',$fw->ENCODING); 125 | // Check local page if pingback-enabled 126 | $parts=parse_url($permalink); 127 | if ((empty($parts['scheme']) || 128 | $parts['host']==$fw->HOST) && 129 | preg_match('/^'.preg_quote($path,'/').'/'. 130 | ($fw->CASELESS?'i':''),$parts['path']) && 131 | $this->enabled($permalink)) { 132 | // Check source 133 | $parts=parse_url($source); 134 | if ((empty($parts['scheme']) || 135 | $parts['host']==$fw->HOST) && 136 | ($req=$web->request($source)) && 137 | $doc->loadhtml($req['body'])) { 138 | $links=$doc->getelementsbytagname('a'); 139 | foreach ($links as $link) { 140 | if ($link->getattribute('href')==$permalink) { 141 | call_user_func_array($func,[$source,$req['body']]); 142 | // Success 143 | die(xmlrpc_encode_request(NULL,$source,$options)); 144 | } 145 | } 146 | // No link to local page 147 | die(xmlrpc_encode_request(NULL,0x11,$options)); 148 | } 149 | // Source failure 150 | die(xmlrpc_encode_request(NULL,0x10,$options)); 151 | } 152 | // Doesn't exist (or not pingback-enabled) 153 | die(xmlrpc_encode_request(NULL,0x21,$options)); 154 | } 155 | // Access denied 156 | die(xmlrpc_encode_request(NULL,0x31,$options)); 157 | } 158 | 159 | /** 160 | * Return transaction history 161 | * @return string 162 | **/ 163 | function log() { 164 | return $this->log; 165 | } 166 | 167 | /** 168 | * Instantiate class 169 | * @return object 170 | **/ 171 | function __construct() { 172 | // Suppress errors caused by invalid HTML structures 173 | libxml_use_internal_errors(TRUE); 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /sample-application/lib/audit.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | //! Data validator 24 | class Audit extends Prefab { 25 | 26 | //@{ User agents 27 | const 28 | UA_Mobile='android|blackberry|phone|ipod|palm|windows\s+ce', 29 | UA_Desktop='bsd|linux|os\s+[x9]|solaris|windows', 30 | UA_Bot='bot|crawl|slurp|spider'; 31 | //@} 32 | 33 | /** 34 | * Return TRUE if string is a valid URL 35 | * @return bool 36 | * @param $str string 37 | **/ 38 | function url($str) { 39 | return is_string(filter_var($str,FILTER_VALIDATE_URL)); 40 | } 41 | 42 | /** 43 | * Return TRUE if string is a valid e-mail address; 44 | * Check DNS MX records if specified 45 | * @return bool 46 | * @param $str string 47 | * @param $mx boolean 48 | **/ 49 | function email($str,$mx=TRUE) { 50 | $hosts=[]; 51 | return is_string(filter_var($str,FILTER_VALIDATE_EMAIL)) && 52 | (!$mx || getmxrr(substr($str,strrpos($str,'@')+1),$hosts)); 53 | } 54 | 55 | /** 56 | * Return TRUE if string is a valid IPV4 address 57 | * @return bool 58 | * @param $addr string 59 | **/ 60 | function ipv4($addr) { 61 | return (bool)filter_var($addr,FILTER_VALIDATE_IP,FILTER_FLAG_IPV4); 62 | } 63 | 64 | /** 65 | * Return TRUE if string is a valid IPV6 address 66 | * @return bool 67 | * @param $addr string 68 | **/ 69 | function ipv6($addr) { 70 | return (bool)filter_var($addr,FILTER_VALIDATE_IP,FILTER_FLAG_IPV6); 71 | } 72 | 73 | /** 74 | * Return TRUE if IP address is within private range 75 | * @return bool 76 | * @param $addr string 77 | **/ 78 | function isprivate($addr) { 79 | return !(bool)filter_var($addr,FILTER_VALIDATE_IP, 80 | FILTER_FLAG_IPV4|FILTER_FLAG_IPV6|FILTER_FLAG_NO_PRIV_RANGE); 81 | } 82 | 83 | /** 84 | * Return TRUE if IP address is within reserved range 85 | * @return bool 86 | * @param $addr string 87 | **/ 88 | function isreserved($addr) { 89 | return !(bool)filter_var($addr,FILTER_VALIDATE_IP, 90 | FILTER_FLAG_IPV4|FILTER_FLAG_IPV6|FILTER_FLAG_NO_RES_RANGE); 91 | } 92 | 93 | /** 94 | * Return TRUE if IP address is neither private nor reserved 95 | * @return bool 96 | * @param $addr string 97 | **/ 98 | function ispublic($addr) { 99 | return (bool)filter_var($addr,FILTER_VALIDATE_IP, 100 | FILTER_FLAG_IPV4|FILTER_FLAG_IPV6| 101 | FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE); 102 | } 103 | 104 | /** 105 | * Return TRUE if user agent is a desktop browser 106 | * @return bool 107 | * @param $agent string 108 | **/ 109 | function isdesktop($agent=NULL) { 110 | if (!isset($agent)) 111 | $agent=Base::instance()->AGENT; 112 | return (bool)preg_match('/('.self::UA_Desktop.')/i',$agent) && 113 | !$this->ismobile($agent); 114 | } 115 | 116 | /** 117 | * Return TRUE if user agent is a mobile device 118 | * @return bool 119 | * @param $agent string 120 | **/ 121 | function ismobile($agent=NULL) { 122 | if (!isset($agent)) 123 | $agent=Base::instance()->AGENT; 124 | return (bool)preg_match('/('.self::UA_Mobile.')/i',$agent); 125 | } 126 | 127 | /** 128 | * Return TRUE if user agent is a Web bot 129 | * @return bool 130 | * @param $agent string 131 | **/ 132 | function isbot($agent=NULL) { 133 | if (!isset($agent)) 134 | $agent=Base::instance()->AGENT; 135 | return (bool)preg_match('/('.self::UA_Bot.')/i',$agent); 136 | } 137 | 138 | /** 139 | * Return TRUE if specified ID has a valid (Luhn) Mod-10 check digit 140 | * @return bool 141 | * @param $id string 142 | **/ 143 | function mod10($id) { 144 | if (!ctype_digit($id)) 145 | return FALSE; 146 | $id=strrev($id); 147 | $sum=0; 148 | for ($i=0,$l=strlen($id);$i<$l;$i++) 149 | $sum+=$id[$i]+$i%2*(($id[$i]>4)*-4+$id[$i]%5); 150 | return !($sum%10); 151 | } 152 | 153 | /** 154 | * Return credit card type if number is valid 155 | * @return string|FALSE 156 | * @param $id string 157 | **/ 158 | function card($id) { 159 | $id=preg_replace('/[^\d]/','',$id); 160 | if ($this->mod10($id)) { 161 | if (preg_match('/^3[47][0-9]{13}$/',$id)) 162 | return 'American Express'; 163 | if (preg_match('/^3(?:0[0-5]|[68][0-9])[0-9]{11}$/',$id)) 164 | return 'Diners Club'; 165 | if (preg_match('/^6(?:011|5[0-9][0-9])[0-9]{12}$/',$id)) 166 | return 'Discover'; 167 | if (preg_match('/^(?:2131|1800|35\d{3})\d{11}$/',$id)) 168 | return 'JCB'; 169 | if (preg_match('/^5[1-5][0-9]{14}$|'. 170 | '^(222[1-9]|2[3-6]\d{2}|27[0-1]\d|2720)\d{12}$/',$id)) 171 | return 'MasterCard'; 172 | if (preg_match('/^4[0-9]{12}(?:[0-9]{3})?$/',$id)) 173 | return 'Visa'; 174 | } 175 | return FALSE; 176 | } 177 | 178 | /** 179 | * Return entropy estimate of a password (NIST 800-63) 180 | * @return int|float 181 | * @param $str string 182 | **/ 183 | function entropy($str) { 184 | $len=strlen($str); 185 | return 4*min($len,1)+($len>1?(2*(min($len,8)-1)):0)+ 186 | ($len>8?(1.5*(min($len,20)-8)):0)+($len>20?($len-20):0)+ 187 | 6*(bool)(preg_match( 188 | '/[A-Z].*?[0-9[:punct:]]|[0-9[:punct:]].*?[A-Z]/',$str)); 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /sample-application/lib/basket.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | //! Session-based pseudo-mapper 24 | class Basket extends Magic { 25 | 26 | //@{ Error messages 27 | const 28 | E_Field='Undefined field %s'; 29 | //@} 30 | 31 | protected 32 | //! Session key 33 | $key, 34 | //! Current item identifier 35 | $id, 36 | //! Current item contents 37 | $item=[]; 38 | 39 | /** 40 | * Return TRUE if field is defined 41 | * @return bool 42 | * @param $key string 43 | **/ 44 | function exists($key) { 45 | return array_key_exists($key,$this->item); 46 | } 47 | 48 | /** 49 | * Assign value to field 50 | * @return scalar|FALSE 51 | * @param $key string 52 | * @param $val scalar 53 | **/ 54 | function set($key,$val) { 55 | return ($key=='_id')?FALSE:($this->item[$key]=$val); 56 | } 57 | 58 | /** 59 | * Retrieve value of field 60 | * @return scalar|FALSE 61 | * @param $key string 62 | **/ 63 | function &get($key) { 64 | if ($key=='_id') 65 | return $this->id; 66 | if (array_key_exists($key,$this->item)) 67 | return $this->item[$key]; 68 | user_error(sprintf(self::E_Field,$key),E_USER_ERROR); 69 | return FALSE; 70 | } 71 | 72 | /** 73 | * Delete field 74 | * @return NULL 75 | * @param $key string 76 | **/ 77 | function clear($key) { 78 | unset($this->item[$key]); 79 | } 80 | 81 | /** 82 | * Return items that match key/value pair; 83 | * If no key/value pair specified, return all items 84 | * @return array 85 | * @param $key string 86 | * @param $val mixed 87 | **/ 88 | function find($key=NULL,$val=NULL) { 89 | $out=[]; 90 | if (isset($_SESSION[$this->key])) { 91 | foreach ($_SESSION[$this->key] as $id=>$item) 92 | if (!isset($key) || 93 | array_key_exists($key,$item) && $item[$key]==$val) { 94 | $obj=clone($this); 95 | $obj->id=$id; 96 | $obj->item=$item; 97 | $out[]=$obj; 98 | } 99 | } 100 | return $out; 101 | } 102 | 103 | /** 104 | * Return first item that matches key/value pair 105 | * @return object|FALSE 106 | * @param $key string 107 | * @param $val mixed 108 | **/ 109 | function findone($key,$val) { 110 | return ($data=$this->find($key,$val))?$data[0]:FALSE; 111 | } 112 | 113 | /** 114 | * Map current item to matching key/value pair 115 | * @return array 116 | * @param $key string 117 | * @param $val mixed 118 | **/ 119 | function load($key,$val) { 120 | if ($found=$this->find($key,$val)) { 121 | $this->id=$found[0]->id; 122 | return $this->item=$found[0]->item; 123 | } 124 | $this->reset(); 125 | return []; 126 | } 127 | 128 | /** 129 | * Return TRUE if current item is empty/undefined 130 | * @return bool 131 | **/ 132 | function dry() { 133 | return !$this->item; 134 | } 135 | 136 | /** 137 | * Return number of items in basket 138 | * @return int 139 | **/ 140 | function count() { 141 | return isset($_SESSION[$this->key])?count($_SESSION[$this->key]):0; 142 | } 143 | 144 | /** 145 | * Save current item 146 | * @return array 147 | **/ 148 | function save() { 149 | if (!$this->id) 150 | $this->id=uniqid(NULL,TRUE); 151 | $_SESSION[$this->key][$this->id]=$this->item; 152 | return $this->item; 153 | } 154 | 155 | /** 156 | * Erase item matching key/value pair 157 | * @return bool 158 | * @param $key string 159 | * @param $val mixed 160 | **/ 161 | function erase($key,$val) { 162 | $found=$this->find($key,$val); 163 | if ($found && $id=$found[0]->id) { 164 | unset($_SESSION[$this->key][$id]); 165 | if ($id==$this->id) 166 | $this->reset(); 167 | return TRUE; 168 | } 169 | return FALSE; 170 | } 171 | 172 | /** 173 | * Reset cursor 174 | * @return NULL 175 | **/ 176 | function reset() { 177 | $this->id=NULL; 178 | $this->item=[]; 179 | } 180 | 181 | /** 182 | * Empty basket 183 | * @return NULL 184 | **/ 185 | function drop() { 186 | unset($_SESSION[$this->key]); 187 | } 188 | 189 | /** 190 | * Hydrate item using hive array variable 191 | * @return NULL 192 | * @param $var array|string 193 | **/ 194 | function copyfrom($var) { 195 | if (is_string($var)) 196 | $var=\Base::instance()->$var; 197 | foreach ($var as $key=>$val) 198 | $this->set($key,$val); 199 | } 200 | 201 | /** 202 | * Populate hive array variable with item contents 203 | * @return NULL 204 | * @param $key string 205 | **/ 206 | function copyto($key) { 207 | $var=&\Base::instance()->ref($key); 208 | foreach ($this->item as $key=>$field) 209 | $var[$key]=$field; 210 | } 211 | 212 | /** 213 | * Check out basket contents 214 | * @return array 215 | **/ 216 | function checkout() { 217 | if (isset($_SESSION[$this->key])) { 218 | $out=$_SESSION[$this->key]; 219 | unset($_SESSION[$this->key]); 220 | return $out; 221 | } 222 | return []; 223 | } 224 | 225 | /** 226 | * Instantiate class 227 | * @return void 228 | * @param $key string 229 | **/ 230 | function __construct($key='basket') { 231 | $this->key=$key; 232 | if (session_status()!=PHP_SESSION_ACTIVE) 233 | session_start(); 234 | Base::instance()->sync('SESSION'); 235 | $this->reset(); 236 | } 237 | 238 | } 239 | -------------------------------------------------------------------------------- /sample-application/lib/auth.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | //! Authorization/authentication plug-in 24 | class Auth { 25 | 26 | //@{ Error messages 27 | const 28 | E_LDAP='LDAP connection failure', 29 | E_SMTP='SMTP connection failure'; 30 | //@} 31 | 32 | protected 33 | //! Auth storage 34 | $storage, 35 | //! Mapper object 36 | $mapper, 37 | //! Storage options 38 | $args; 39 | 40 | /** 41 | * Jig storage handler 42 | * @return bool 43 | * @param $id string 44 | * @param $pw string 45 | * @param $realm string 46 | **/ 47 | protected function _jig($id,$pw,$realm) { 48 | return (bool) 49 | call_user_func_array( 50 | [$this->mapper,'load'], 51 | [ 52 | array_merge( 53 | [ 54 | '@'.$this->args['id'].'==? AND '. 55 | '@'.$this->args['pw'].'==?'. 56 | (isset($this->args['realm'])? 57 | (' AND @'.$this->args['realm'].'==?'):''), 58 | $id,$pw 59 | ], 60 | (isset($this->args['realm'])?[$realm]:[]) 61 | ) 62 | ] 63 | ); 64 | } 65 | 66 | /** 67 | * MongoDB storage handler 68 | * @return bool 69 | * @param $id string 70 | * @param $pw string 71 | * @param $realm string 72 | **/ 73 | protected function _mongo($id,$pw,$realm) { 74 | return (bool) 75 | $this->mapper->load( 76 | [ 77 | $this->args['id']=>$id, 78 | $this->args['pw']=>$pw 79 | ]+ 80 | (isset($this->args['realm'])? 81 | [$this->args['realm']=>$realm]:[]) 82 | ); 83 | } 84 | 85 | /** 86 | * SQL storage handler 87 | * @return bool 88 | * @param $id string 89 | * @param $pw string 90 | * @param $realm string 91 | **/ 92 | protected function _sql($id,$pw,$realm) { 93 | return (bool) 94 | call_user_func_array( 95 | [$this->mapper,'load'], 96 | [ 97 | array_merge( 98 | [ 99 | $this->args['id'].'=? AND '. 100 | $this->args['pw'].'=?'. 101 | (isset($this->args['realm'])? 102 | (' AND '.$this->args['realm'].'=?'):''), 103 | $id,$pw 104 | ], 105 | (isset($this->args['realm'])?[$realm]:[]) 106 | ) 107 | ] 108 | ); 109 | } 110 | 111 | /** 112 | * LDAP storage handler 113 | * @return bool 114 | * @param $id string 115 | * @param $pw string 116 | **/ 117 | protected function _ldap($id,$pw) { 118 | $dc=@ldap_connect($this->args['dc']); 119 | if ($dc && 120 | ldap_set_option($dc,LDAP_OPT_PROTOCOL_VERSION,3) && 121 | ldap_set_option($dc,LDAP_OPT_REFERRALS,0) && 122 | ldap_bind($dc,$this->args['rdn'],$this->args['pw']) && 123 | ($result=ldap_search($dc,$this->args['base_dn'], 124 | $this->args['uid'].'='.$id)) && 125 | ldap_count_entries($dc,$result) && 126 | ($info=ldap_get_entries($dc,$result)) && 127 | @ldap_bind($dc,$info[0]['dn'],$pw) && 128 | @ldap_close($dc)) { 129 | return $info[0][$this->args['uid']][0]==$id; 130 | } 131 | user_error(self::E_LDAP,E_USER_ERROR); 132 | } 133 | 134 | /** 135 | * SMTP storage handler 136 | * @return bool 137 | * @param $id string 138 | * @param $pw string 139 | **/ 140 | protected function _smtp($id,$pw) { 141 | $socket=@fsockopen( 142 | (strtolower($this->args['scheme'])=='ssl'? 143 | 'ssl://':'').$this->args['host'], 144 | $this->args['port']); 145 | $dialog=function($cmd=NULL) use($socket) { 146 | if (!is_null($cmd)) 147 | fputs($socket,$cmd."\r\n"); 148 | $reply=''; 149 | while (!feof($socket) && 150 | ($info=stream_get_meta_data($socket)) && 151 | !$info['timed_out'] && $str=fgets($socket,4096)) { 152 | $reply.=$str; 153 | if (preg_match('/(?:^|\n)\d{3} .+\r\n/s', 154 | $reply)) 155 | break; 156 | } 157 | return $reply; 158 | }; 159 | if ($socket) { 160 | stream_set_blocking($socket,TRUE); 161 | $dialog(); 162 | $fw=Base::instance(); 163 | $dialog('EHLO '.$fw->HOST); 164 | if (strtolower($this->args['scheme'])=='tls') { 165 | $dialog('STARTTLS'); 166 | stream_socket_enable_crypto( 167 | $socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT); 168 | $dialog('EHLO '.$fw->HOST); 169 | } 170 | // Authenticate 171 | $dialog('AUTH LOGIN'); 172 | $dialog(base64_encode($id)); 173 | $reply=$dialog(base64_encode($pw)); 174 | $dialog('QUIT'); 175 | fclose($socket); 176 | return (bool)preg_match('/^235 /',$reply); 177 | } 178 | user_error(self::E_SMTP,E_USER_ERROR); 179 | } 180 | 181 | /** 182 | * Login auth mechanism 183 | * @return bool 184 | * @param $id string 185 | * @param $pw string 186 | * @param $realm string 187 | **/ 188 | function login($id,$pw,$realm=NULL) { 189 | return $this->{'_'.$this->storage}($id,$pw,$realm); 190 | } 191 | 192 | /** 193 | * HTTP basic auth mechanism 194 | * @return bool 195 | * @param $func callback 196 | **/ 197 | function basic($func=NULL) { 198 | $fw=Base::instance(); 199 | $realm=$fw->REALM; 200 | $hdr=NULL; 201 | if (isset($_SERVER['HTTP_AUTHORIZATION'])) 202 | $hdr=$_SERVER['HTTP_AUTHORIZATION']; 203 | elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) 204 | $hdr=$_SERVER['REDIRECT_HTTP_AUTHORIZATION']; 205 | if (!empty($hdr)) 206 | list($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW'])= 207 | explode(':',base64_decode(substr($hdr,6))); 208 | if (isset($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']) && 209 | $this->login( 210 | $_SERVER['PHP_AUTH_USER'], 211 | $func? 212 | $fw->call($func,$_SERVER['PHP_AUTH_PW']): 213 | $_SERVER['PHP_AUTH_PW'], 214 | $realm 215 | )) 216 | return TRUE; 217 | if (PHP_SAPI!='cli') 218 | header('WWW-Authenticate: Basic realm="'.$realm.'"'); 219 | $fw->status(401); 220 | return FALSE; 221 | } 222 | 223 | /** 224 | * Instantiate class 225 | * @return object 226 | * @param $storage string|object 227 | * @param $args array 228 | **/ 229 | function __construct($storage,array $args=NULL) { 230 | if (is_object($storage) && is_a($storage,'DB\Cursor')) { 231 | $this->storage=$storage->dbtype(); 232 | $this->mapper=$storage; 233 | unset($ref); 234 | } 235 | else 236 | $this->storage=$storage; 237 | $this->args=$args; 238 | } 239 | 240 | } 241 | -------------------------------------------------------------------------------- /sample-application/lib/web/openid.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace Web; 24 | 25 | //! OpenID consumer 26 | class OpenID extends \Magic { 27 | 28 | protected 29 | //! OpenID provider endpoint URL 30 | $url, 31 | //! HTTP request parameters 32 | $args=[]; 33 | 34 | /** 35 | * Determine OpenID provider 36 | * @return string|FALSE 37 | * @param $proxy string 38 | **/ 39 | protected function discover($proxy) { 40 | // Normalize 41 | if (!preg_match('/https?:\/\//i',$this->args['endpoint'])) 42 | $this->args['endpoint']='http://'.$this->args['endpoint']; 43 | $url=parse_url($this->args['endpoint']); 44 | // Remove fragment; reconnect parts 45 | $this->args['endpoint']=$url['scheme'].'://'. 46 | (isset($url['user'])? 47 | ($url['user']. 48 | (isset($url['pass'])?(':'.$url['pass']):'').'@'):''). 49 | strtolower($url['host']).(isset($url['path'])?$url['path']:'/'). 50 | (isset($url['query'])?('?'.$url['query']):''); 51 | // HTML-based discovery of OpenID provider 52 | $req=\Web::instance()-> 53 | request($this->args['endpoint'],['proxy'=>$proxy]); 54 | if (!$req) 55 | return FALSE; 56 | $type=array_values(preg_grep('/Content-Type:/',$req['headers'])); 57 | if ($type && 58 | preg_match('/application\/xrds\+xml|text\/xml/',$type[0]) && 59 | ($sxml=simplexml_load_string($req['body'])) && 60 | ($xrds=json_decode(json_encode($sxml),TRUE)) && 61 | isset($xrds['XRD'])) { 62 | // XRDS document 63 | $svc=$xrds['XRD']['Service']; 64 | if (isset($svc[0])) 65 | $svc=$svc[0]; 66 | $svc_type=is_array($svc['Type'])?$svc['Type']:array($svc['Type']); 67 | if (preg_grep('/http:\/\/specs\.openid\.net\/auth\/2.0\/'. 68 | '(?:server|signon)/',$svc_type)) { 69 | $this->args['provider']=$svc['URI']; 70 | if (isset($svc['LocalID'])) 71 | $this->args['localidentity']=$svc['LocalID']; 72 | elseif (isset($svc['CanonicalID'])) 73 | $this->args['localidentity']=$svc['CanonicalID']; 74 | } 75 | $this->args['server']=$svc['URI']; 76 | if (isset($svc['Delegate'])) 77 | $this->args['delegate']=$svc['Delegate']; 78 | } 79 | else { 80 | $len=strlen($req['body']); 81 | $ptr=0; 82 | // Parse document 83 | while ($ptr<$len) 84 | if (preg_match( 85 | '/^/is', 87 | substr($req['body'],$ptr),$parts)) { 88 | if ($parts[1] && 89 | // Process attributes 90 | preg_match_all('/\b(rel|href)\h*=\h*'. 91 | '(?:"(.+?)"|\'(.+?)\')/s',$parts[1],$attr, 92 | PREG_SET_ORDER)) { 93 | $node=[]; 94 | foreach ($attr as $kv) 95 | $node[$kv[1]]=isset($kv[2])?$kv[2]:$kv[3]; 96 | if (isset($node['rel']) && 97 | preg_match('/openid2?\.(\w+)/', 98 | $node['rel'],$var) && 99 | isset($node['href'])) 100 | $this->args[$var[1]]=$node['href']; 101 | 102 | } 103 | $ptr+=strlen($parts[0]); 104 | } 105 | else 106 | $ptr++; 107 | } 108 | // Get OpenID provider's endpoint URL 109 | if (isset($this->args['provider'])) { 110 | // OpenID 2.0 111 | $this->args['ns']='http://specs.openid.net/auth/2.0'; 112 | if (isset($this->args['localidentity'])) 113 | $this->args['identity']=$this->args['localidentity']; 114 | if (isset($this->args['trust_root'])) 115 | $this->args['realm']=$this->args['trust_root']; 116 | } 117 | elseif (isset($this->args['server'])) { 118 | // OpenID 1.1 119 | $this->args['ns']='http://openid.net/signon/1.1'; 120 | if (isset($this->args['delegate'])) 121 | $this->args['identity']=$this->args['delegate']; 122 | } 123 | if (isset($this->args['provider'])) { 124 | // OpenID 2.0 125 | if (empty($this->args['claimed_id'])) 126 | $this->args['claimed_id']=$this->args['identity']; 127 | return $this->args['provider']; 128 | } 129 | elseif (isset($this->args['server'])) 130 | // OpenID 1.1 131 | return $this->args['server']; 132 | else 133 | return FALSE; 134 | } 135 | 136 | /** 137 | * Initiate OpenID authentication sequence; Return FALSE on failure 138 | * or redirect to OpenID provider URL 139 | * @return bool 140 | * @param $proxy string 141 | * @param $attr array 142 | * @param $reqd string|array 143 | **/ 144 | function auth($proxy=NULL,$attr=[],array $reqd=NULL) { 145 | $fw=\Base::instance(); 146 | $root=$fw->SCHEME.'://'.$fw->HOST; 147 | if (empty($this->args['trust_root'])) 148 | $this->args['trust_root']=$root.$fw->BASE.'/'; 149 | if (empty($this->args['return_to'])) 150 | $this->args['return_to']=$root.$_SERVER['REQUEST_URI']; 151 | $this->args['mode']='checkid_setup'; 152 | if ($this->url=$this->discover($proxy)) { 153 | if ($attr) { 154 | $this->args['ns.ax']='http://openid.net/srv/ax/1.0'; 155 | $this->args['ax.mode']='fetch_request'; 156 | foreach ($attr as $key=>$val) 157 | $this->args['ax.type.'.$key]=$val; 158 | $this->args['ax.required']=is_string($reqd)? 159 | $reqd:implode(',',$reqd); 160 | } 161 | $var=[]; 162 | foreach ($this->args as $key=>$val) 163 | $var['openid.'.$key]=$val; 164 | $fw->reroute($this->url.'?'.http_build_query($var)); 165 | } 166 | return FALSE; 167 | } 168 | 169 | /** 170 | * Return TRUE if OpenID verification was successful 171 | * @return bool 172 | * @param $proxy string 173 | **/ 174 | function verified($proxy=NULL) { 175 | preg_match_all('/(?<=^|&)openid\.([^=]+)=([^&]+)/', 176 | $_SERVER['QUERY_STRING'],$matches,PREG_SET_ORDER); 177 | foreach ($matches as $match) 178 | $this->args[$match[1]]=urldecode($match[2]); 179 | if (isset($this->args['mode']) && 180 | $this->args['mode']!='error' && 181 | $this->url=$this->discover($proxy)) { 182 | $this->args['mode']='check_authentication'; 183 | $var=[]; 184 | foreach ($this->args as $key=>$val) 185 | $var['openid.'.$key]=$val; 186 | $req=\Web::instance()->request( 187 | $this->url, 188 | [ 189 | 'method'=>'POST', 190 | 'content'=>http_build_query($var), 191 | 'proxy'=>$proxy 192 | ] 193 | ); 194 | return (bool)preg_match('/is_valid:true/i',$req['body']); 195 | } 196 | return FALSE; 197 | } 198 | 199 | /** 200 | * Return OpenID response fields 201 | * @return array 202 | **/ 203 | function response() { 204 | return $this->args; 205 | } 206 | 207 | /** 208 | * Return TRUE if OpenID request parameter exists 209 | * @return bool 210 | * @param $key string 211 | **/ 212 | function exists($key) { 213 | return isset($this->args[$key]); 214 | } 215 | 216 | /** 217 | * Bind value to OpenID request parameter 218 | * @return string 219 | * @param $key string 220 | * @param $val string 221 | **/ 222 | function set($key,$val) { 223 | return $this->args[$key]=$val; 224 | } 225 | 226 | /** 227 | * Return value of OpenID request parameter 228 | * @return mixed 229 | * @param $key string 230 | **/ 231 | function &get($key) { 232 | if (isset($this->args[$key])) 233 | $val=&$this->args[$key]; 234 | else 235 | $val=NULL; 236 | return $val; 237 | } 238 | 239 | /** 240 | * Remove OpenID request parameter 241 | * @return NULL 242 | * @param $key 243 | **/ 244 | function clear($key) { 245 | unset($this->args[$key]); 246 | } 247 | 248 | } 249 | -------------------------------------------------------------------------------- /lib/IPay.php: -------------------------------------------------------------------------------- 1 | sync('SESSION'); 34 | if ($options == null) 35 | if ($f3->exists('IPAY')) 36 | $options = $f3->get('IPAY'); 37 | else 38 | $f3->error(500, 'No configuration options set for iPay on Fat Free Framework'); 39 | if ($options['endpoint'] == "production") { 40 | $this->live = '1'; 41 | } else { 42 | $this->live = '0'; 43 | } 44 | if ($options['crl']=="http"){ 45 | $this->crl= '0'; 46 | }elseif ($options['crl']=="data_stream"){ 47 | $this->crl= '1'; 48 | } 49 | else{ 50 | $this->crl= '2'; 51 | } 52 | $this->iPaySettings['vendorID'] = $options['vendorID']; 53 | $this->iPaySettings['hashkey'] = $options['hashkey']; 54 | $this->iPaySettings['callback'] = $options['call_back']; 55 | $this->iPaySettings['currency'] = $options['currency']; 56 | $this->iPaySettings['customer_email_notification'] = $options['cst']; 57 | //Define Channel status in requests 58 | $this->iPaySettings['mpesa'] = $options['mpesa']; 59 | $this->iPaySettings['airtel'] = $options['airtel']; 60 | $this->iPaySettings['equity'] = $options['equity']; 61 | $this->iPaySettings['mobilebanking'] = $options['mobilebanking']; 62 | $this->iPaySettings['debitcard'] = $options['debitcard']; 63 | $this->iPaySettings['creditcard'] = $options['creditcard']; 64 | $this->iPaySettings['mkoporahisi'] = $options['mkoporahisi']; 65 | $this->iPaySettings['saida'] = $options['saida']; 66 | $this->iPaySettings['autopay'] = $options['autopay']; 67 | //End define Channel status in requests 68 | if ($options['log']) { 69 | $this->logger = new Log('iPay.log'); 70 | } 71 | } 72 | /** 73 | * Build array of line items & calculating item total. 74 | * @param $item_name string 75 | * @param $item_quantity integer 76 | * @param $item_price string 77 | */ 78 | function setLineItem($item_name, $item_quantity = 1, $item_price) 79 | { 80 | $i = $this->item_counter++; 81 | $this->line_items["L_PAYMENTREQUEST_0_NAME$i"] = $item_name; 82 | $this->line_items["L_PAYMENTREQUEST_0_QTY$i"] = $item_quantity; 83 | $this->line_items["L_PAYMENTREQUEST_0_AMOUNT$i"] = $item_price; 84 | $this->item_total += ($item_quantity * $item_price); 85 | } 86 | /** 87 | * Create iPay Payment Session. 88 | * @param $orderID string 89 | * @param $totalAmount integer 90 | * @param $email string 91 | * @param $telephone string 92 | * @param $p1 string 93 | * @param $p2 string 94 | * @param $p3 string 95 | * @param $p4 string 96 | */ 97 | function process_iPay_payment($orderID,$totalAmount,$email,$telephone,$p1="",$p2="",$p3="",$p4=""){ 98 | $web = \Web::instance(); 99 | //initialize iPay Variables from settings 100 | $getLiveStatus = $this->live; 101 | $getVendorID = $this->iPaySettings['vendorID']; 102 | $getHashkey = $this->iPaySettings['hashkey']; 103 | $getCallBack = $this->iPaySettings['callback']; 104 | $getCurrency = $this->iPaySettings['currency']; 105 | $getCst = $this->iPaySettings['customer_email_notification']; 106 | $getMpesa = $this->iPaySettings['mpesa']; 107 | $getAirtel = $this->iPaySettings['airtel']; 108 | $getEquity = $this->iPaySettings['equity']; 109 | $getMobileBanking = $this->iPaySettings['mobilebanking']; 110 | $getDebitCard = $this->iPaySettings['debitcard']; 111 | $getCreditCard = $this->iPaySettings['creditcard']; 112 | $getMkopoRahisi = $this->iPaySettings['mkoporahisi']; 113 | $getSaida = $this->iPaySettings['saida']; 114 | $getAutopay = $this->iPaySettings['autopay']; 115 | $getCrl= $this->crl; 116 | $invoiceNumber=$orderID; 117 | //End initialize iPay Variables from settings 118 | //create iPay concatenated data string 119 | $datastring = $getLiveStatus.$orderID.$invoiceNumber.$totalAmount.$telephone.$email.$getVendorID.$getCurrency.$p1.$p2.$p3.$p4.$getCallBack.$getCst.$getCrl; 120 | //generate hash value for hsh parameter using the datastring 121 | $finalhashValue = hash_hmac("sha1", $datastring, $getHashkey); 122 | //generate iPay payment form and redirect to the site immediately to start checkout 123 | $ipay_endpoint="https://payments.ipayafrica.com/v3/ke"; 124 | $sendtoIpay =' 125 | 129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | 156 |
' 157 | ; 158 | 159 | return $sendtoIpay; 160 | } 161 | 162 | /** 163 | * Copy basket() to iPay Checkout 164 | * Transfer your basket details to the iPay Checkout 165 | * Returns a total value of items 166 | * @param $basket object 167 | * @param $name string 168 | * @param $amount string 169 | */ 170 | function copyBasket($basket, $name = 'name', $quantity = 'qty', $amount = 'amount') 171 | { 172 | $totalamount = 0; 173 | foreach ($basket as $lineitem) { 174 | 175 | if (empty($lineitem->{$quantity})) { 176 | $lineitem->{$quantity} = 1; 177 | } 178 | 179 | $this->setLineItem($lineitem->{$name}, $lineitem->{$quantity}, $lineitem->{$amount}); 180 | $totalamount += $lineitem->{$amount} * $lineitem->{$quantity}; 181 | } 182 | 183 | return $totalamount; 184 | } 185 | 186 | } -------------------------------------------------------------------------------- /sample-application/lib/IPay.php: -------------------------------------------------------------------------------- 1 | sync('SESSION'); 34 | if ($options == null) 35 | if ($f3->exists('IPAY')) 36 | $options = $f3->get('IPAY'); 37 | else 38 | $f3->error(500, 'No configuration options set for iPay on Fat Free Framework'); 39 | if ($options['endpoint'] == "production") { 40 | $this->live = '1'; 41 | } else { 42 | $this->live = '0'; 43 | } 44 | if ($options['crl']=="http"){ 45 | $this->crl= '0'; 46 | }elseif ($options['crl']=="data_stream"){ 47 | $this->crl= '1'; 48 | } 49 | else{ 50 | $this->crl= '2'; 51 | } 52 | $this->iPaySettings['vendorID'] = $options['vendorID']; 53 | $this->iPaySettings['hashkey'] = $options['hashkey']; 54 | $this->iPaySettings['callback'] = $options['call_back']; 55 | $this->iPaySettings['currency'] = $options['currency']; 56 | $this->iPaySettings['customer_email_notification'] = $options['cst']; 57 | //Define Channel status in requests 58 | $this->iPaySettings['mpesa'] = $options['mpesa']; 59 | $this->iPaySettings['airtel'] = $options['airtel']; 60 | $this->iPaySettings['equity'] = $options['equity']; 61 | $this->iPaySettings['mobilebanking'] = $options['mobilebanking']; 62 | $this->iPaySettings['debitcard'] = $options['debitcard']; 63 | $this->iPaySettings['creditcard'] = $options['creditcard']; 64 | $this->iPaySettings['mkoporahisi'] = $options['mkoporahisi']; 65 | $this->iPaySettings['saida'] = $options['saida']; 66 | $this->iPaySettings['autopay'] = $options['autopay']; 67 | //End define Channel status in requests 68 | if ($options['log']) { 69 | $this->logger = new Log('iPay.log'); 70 | } 71 | } 72 | /** 73 | * Build array of line items & calculating item total. 74 | * @param $item_name string 75 | * @param $item_quantity integer 76 | * @param $item_price string 77 | */ 78 | function setLineItem($item_name, $item_quantity = 1, $item_price) 79 | { 80 | $i = $this->item_counter++; 81 | $this->line_items["L_PAYMENTREQUEST_0_NAME$i"] = $item_name; 82 | $this->line_items["L_PAYMENTREQUEST_0_QTY$i"] = $item_quantity; 83 | $this->line_items["L_PAYMENTREQUEST_0_AMOUNT$i"] = $item_price; 84 | $this->item_total += ($item_quantity * $item_price); 85 | } 86 | /** 87 | * Create iPay Payment Session. 88 | * @param $orderID string 89 | * @param $totalAmount integer 90 | * @param $email string 91 | * @param $telephone string 92 | * @param $p1 string 93 | * @param $p2 string 94 | * @param $p3 string 95 | * @param $p4 string 96 | */ 97 | function process_iPay_payment($orderID,$totalAmount,$email,$telephone,$p1="",$p2="",$p3="",$p4=""){ 98 | $web = \Web::instance(); 99 | //initialize iPay Variables from settings 100 | $getLiveStatus = $this->live; 101 | $getVendorID = $this->iPaySettings['vendorID']; 102 | $getHashkey = $this->iPaySettings['hashkey']; 103 | $getCallBack = $this->iPaySettings['callback']; 104 | $getCurrency = $this->iPaySettings['currency']; 105 | $getCst = $this->iPaySettings['customer_email_notification']; 106 | $getMpesa = $this->iPaySettings['mpesa']; 107 | $getAirtel = $this->iPaySettings['airtel']; 108 | $getEquity = $this->iPaySettings['equity']; 109 | $getMobileBanking = $this->iPaySettings['mobilebanking']; 110 | $getDebitCard = $this->iPaySettings['debitcard']; 111 | $getCreditCard = $this->iPaySettings['creditcard']; 112 | $getMkopoRahisi = $this->iPaySettings['mkoporahisi']; 113 | $getSaida = $this->iPaySettings['saida']; 114 | $getAutopay = $this->iPaySettings['autopay']; 115 | $getCrl= $this->crl; 116 | $invoiceNumber=$orderID; 117 | //End initialize iPay Variables from settings 118 | //create iPay concatenated data string 119 | $datastring = $getLiveStatus.$orderID.$invoiceNumber.$totalAmount.$telephone.$email.$getVendorID.$getCurrency.$p1.$p2.$p3.$p4.$getCallBack.$getCst.$getCrl; 120 | //generate hash value for hsh parameter using the datastring 121 | $finalhashValue = hash_hmac("sha1", $datastring, $getHashkey); 122 | //generate iPay payment form and redirect to the site immediately to start checkout 123 | $ipay_endpoint="https://payments.ipayafrica.com/v3/ke"; 124 | $sendtoIpay =' 125 | 129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | 156 |
' 157 | ; 158 | 159 | return $sendtoIpay; 160 | } 161 | 162 | /** 163 | * Copy basket() to iPay Checkout 164 | * Transfer your basket details to the iPay Checkout 165 | * Returns a total value of items 166 | * @param $basket object 167 | * @param $name string 168 | * @param $amount string 169 | */ 170 | function copyBasket($basket, $name = 'name', $quantity = 'qty', $amount = 'amount') 171 | { 172 | $totalamount = 0; 173 | foreach ($basket as $lineitem) { 174 | 175 | if (empty($lineitem->{$quantity})) { 176 | $lineitem->{$quantity} = 1; 177 | } 178 | 179 | $this->setLineItem($lineitem->{$name}, $lineitem->{$quantity}, $lineitem->{$amount}); 180 | $totalamount += $lineitem->{$amount} * $lineitem->{$quantity}; 181 | } 182 | 183 | return $totalamount; 184 | } 185 | 186 | } -------------------------------------------------------------------------------- /sample-application/lib/db/cursor.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace DB; 24 | 25 | //! Simple cursor implementation 26 | abstract class Cursor extends \Magic implements \IteratorAggregate { 27 | 28 | //@{ Error messages 29 | const 30 | E_Field='Undefined field %s'; 31 | //@} 32 | 33 | protected 34 | //! Query results 35 | $query=[], 36 | //! Current position 37 | $ptr=0, 38 | //! Event listeners 39 | $trigger=[]; 40 | 41 | /** 42 | * Return database type 43 | * @return string 44 | **/ 45 | abstract function dbtype(); 46 | 47 | /** 48 | * Return field names 49 | * @return array 50 | **/ 51 | abstract function fields(); 52 | 53 | /** 54 | * Return fields of mapper object as an associative array 55 | * @return array 56 | * @param $obj object 57 | **/ 58 | abstract function cast($obj=NULL); 59 | 60 | /** 61 | * Return records (array of mapper objects) that match criteria 62 | * @return array 63 | * @param $filter string|array 64 | * @param $options array 65 | * @param $ttl int 66 | **/ 67 | abstract function find($filter=NULL,array $options=NULL,$ttl=0); 68 | 69 | /** 70 | * Count records that match criteria 71 | * @return int 72 | * @param $filter array 73 | * @param $options array 74 | * @param $ttl int 75 | **/ 76 | abstract function count($filter=NULL,array $options=NULL,$ttl=0); 77 | 78 | /** 79 | * Insert new record 80 | * @return array 81 | **/ 82 | abstract function insert(); 83 | 84 | /** 85 | * Update current record 86 | * @return array 87 | **/ 88 | abstract function update(); 89 | 90 | /** 91 | * Hydrate mapper object using hive array variable 92 | * @return NULL 93 | * @param $var array|string 94 | * @param $func callback 95 | **/ 96 | abstract function copyfrom($var,$func=NULL); 97 | 98 | /** 99 | * Populate hive array variable with mapper fields 100 | * @return NULL 101 | * @param $key string 102 | **/ 103 | abstract function copyto($key); 104 | 105 | /** 106 | * Get cursor's equivalent external iterator 107 | * Causes a fatal error in PHP 5.3.5 if uncommented 108 | * return ArrayIterator 109 | **/ 110 | abstract function getiterator(); 111 | 112 | 113 | /** 114 | * Return TRUE if current cursor position is not mapped to any record 115 | * @return bool 116 | **/ 117 | function dry() { 118 | return empty($this->query[$this->ptr]); 119 | } 120 | 121 | /** 122 | * Return first record (mapper object) that matches criteria 123 | * @return static|FALSE 124 | * @param $filter string|array 125 | * @param $options array 126 | * @param $ttl int 127 | **/ 128 | function findone($filter=NULL,array $options=NULL,$ttl=0) { 129 | if (!$options) 130 | $options=[]; 131 | // Override limit 132 | $options['limit']=1; 133 | return ($data=$this->find($filter,$options,$ttl))?$data[0]:FALSE; 134 | } 135 | 136 | /** 137 | * Return array containing subset of records matching criteria, 138 | * total number of records in superset, specified limit, number of 139 | * subsets available, and actual subset position 140 | * @return array 141 | * @param $pos int 142 | * @param $size int 143 | * @param $filter string|array 144 | * @param $options array 145 | * @param $ttl int 146 | * @param $bounce bool 147 | **/ 148 | function paginate( 149 | $pos=0,$size=10,$filter=NULL,array $options=NULL,$ttl=0,$bounce=TRUE) { 150 | $total=$this->count($filter,$options,$ttl); 151 | $count=ceil($total/$size); 152 | if ($bounce) 153 | $pos=max(0,min($pos,$count-1)); 154 | return [ 155 | 'subset'=>($bounce || $pos<$count)?$this->find($filter, 156 | array_merge( 157 | $options?:[], 158 | ['limit'=>$size,'offset'=>$pos*$size] 159 | ), 160 | $ttl 161 | ):[], 162 | 'total'=>$total, 163 | 'limit'=>$size, 164 | 'count'=>$count, 165 | 'pos'=>$bounce?($pos<$count?$pos:0):$pos 166 | ]; 167 | } 168 | 169 | /** 170 | * Map to first record that matches criteria 171 | * @return array|FALSE 172 | * @param $filter string|array 173 | * @param $options array 174 | * @param $ttl int 175 | **/ 176 | function load($filter=NULL,array $options=NULL,$ttl=0) { 177 | $this->reset(); 178 | return ($this->query=$this->find($filter,$options,$ttl)) && 179 | $this->skip(0)?$this->query[$this->ptr]:FALSE; 180 | } 181 | 182 | /** 183 | * Return the count of records loaded 184 | * @return int 185 | **/ 186 | function loaded() { 187 | return count($this->query); 188 | } 189 | 190 | /** 191 | * Map to first record in cursor 192 | * @return mixed 193 | **/ 194 | function first() { 195 | return $this->skip(-$this->ptr); 196 | } 197 | 198 | /** 199 | * Map to last record in cursor 200 | * @return mixed 201 | **/ 202 | function last() { 203 | return $this->skip(($ofs=count($this->query)-$this->ptr)?$ofs-1:0); 204 | } 205 | 206 | /** 207 | * Map to nth record relative to current cursor position 208 | * @return mixed 209 | * @param $ofs int 210 | **/ 211 | function skip($ofs=1) { 212 | $this->ptr+=$ofs; 213 | return $this->ptr>-1 && $this->ptrquery)? 214 | $this->query[$this->ptr]:FALSE; 215 | } 216 | 217 | /** 218 | * Map next record 219 | * @return mixed 220 | **/ 221 | function next() { 222 | return $this->skip(); 223 | } 224 | 225 | /** 226 | * Map previous record 227 | * @return mixed 228 | **/ 229 | function prev() { 230 | return $this->skip(-1); 231 | } 232 | 233 | /** 234 | * Return whether current iterator position is valid. 235 | */ 236 | function valid() { 237 | return !$this->dry(); 238 | } 239 | 240 | /** 241 | * Save mapped record 242 | * @return mixed 243 | **/ 244 | function save() { 245 | return $this->query?$this->update():$this->insert(); 246 | } 247 | 248 | /** 249 | * Delete current record 250 | * @return int|bool 251 | **/ 252 | function erase() { 253 | $this->query=array_slice($this->query,0,$this->ptr,TRUE)+ 254 | array_slice($this->query,$this->ptr,NULL,TRUE); 255 | $this->skip(0); 256 | } 257 | 258 | /** 259 | * Define onload trigger 260 | * @return callback 261 | * @param $func callback 262 | **/ 263 | function onload($func) { 264 | return $this->trigger['load']=$func; 265 | } 266 | 267 | /** 268 | * Define beforeinsert trigger 269 | * @return callback 270 | * @param $func callback 271 | **/ 272 | function beforeinsert($func) { 273 | return $this->trigger['beforeinsert']=$func; 274 | } 275 | 276 | /** 277 | * Define afterinsert trigger 278 | * @return callback 279 | * @param $func callback 280 | **/ 281 | function afterinsert($func) { 282 | return $this->trigger['afterinsert']=$func; 283 | } 284 | 285 | /** 286 | * Define oninsert trigger 287 | * @return callback 288 | * @param $func callback 289 | **/ 290 | function oninsert($func) { 291 | return $this->afterinsert($func); 292 | } 293 | 294 | /** 295 | * Define beforeupdate trigger 296 | * @return callback 297 | * @param $func callback 298 | **/ 299 | function beforeupdate($func) { 300 | return $this->trigger['beforeupdate']=$func; 301 | } 302 | 303 | /** 304 | * Define afterupdate trigger 305 | * @return callback 306 | * @param $func callback 307 | **/ 308 | function afterupdate($func) { 309 | return $this->trigger['afterupdate']=$func; 310 | } 311 | 312 | /** 313 | * Define onupdate trigger 314 | * @return callback 315 | * @param $func callback 316 | **/ 317 | function onupdate($func) { 318 | return $this->afterupdate($func); 319 | } 320 | 321 | /** 322 | * Define beforesave trigger 323 | * @return callback 324 | * @param $func callback 325 | **/ 326 | function beforesave($func) { 327 | $this->trigger['beforeinsert']=$func; 328 | $this->trigger['beforeupdate']=$func; 329 | return $func; 330 | } 331 | 332 | /** 333 | * Define aftersave trigger 334 | * @return callback 335 | * @param $func callback 336 | **/ 337 | function aftersave($func) { 338 | $this->trigger['afterinsert']=$func; 339 | $this->trigger['afterupdate']=$func; 340 | return $func; 341 | } 342 | 343 | /** 344 | * Define onsave trigger 345 | * @return callback 346 | * @param $func callback 347 | **/ 348 | function onsave($func) { 349 | return $this->aftersave($func); 350 | } 351 | 352 | /** 353 | * Define beforeerase trigger 354 | * @return callback 355 | * @param $func callback 356 | **/ 357 | function beforeerase($func) { 358 | return $this->trigger['beforeerase']=$func; 359 | } 360 | 361 | /** 362 | * Define aftererase trigger 363 | * @return callback 364 | * @param $func callback 365 | **/ 366 | function aftererase($func) { 367 | return $this->trigger['aftererase']=$func; 368 | } 369 | 370 | /** 371 | * Define onerase trigger 372 | * @return callback 373 | * @param $func callback 374 | **/ 375 | function onerase($func) { 376 | return $this->aftererase($func); 377 | } 378 | 379 | /** 380 | * Reset cursor 381 | * @return NULL 382 | **/ 383 | function reset() { 384 | $this->query=[]; 385 | $this->ptr=0; 386 | } 387 | 388 | } 389 | -------------------------------------------------------------------------------- /sample-application/lib/template.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | //! XML-style template engine 24 | class Template extends Preview { 25 | 26 | //@{ Error messages 27 | const 28 | E_Method='Call to undefined method %s()'; 29 | //@} 30 | 31 | protected 32 | //! Template tags 33 | $tags, 34 | //! Custom tag handlers 35 | $custom=[]; 36 | 37 | /** 38 | * Template -set- tag handler 39 | * @return string 40 | * @param $node array 41 | **/ 42 | protected function _set(array $node) { 43 | $out=''; 44 | foreach ($node['@attrib'] as $key=>$val) 45 | $out.='$'.$key.'='. 46 | (preg_match('/\{\{(.+?)\}\}/',$val)? 47 | $this->token($val): 48 | Base::instance()->stringify($val)).'; '; 49 | return ''; 50 | } 51 | 52 | /** 53 | * Template -include- tag handler 54 | * @return string 55 | * @param $node array 56 | **/ 57 | protected function _include(array $node) { 58 | $attrib=$node['@attrib']; 59 | $hive=isset($attrib['with']) && 60 | ($attrib['with']=$this->token($attrib['with'])) && 61 | preg_match_all('/(\w+)\h*=\h*(.+?)(?=,|$)/', 62 | $attrib['with'],$pairs,PREG_SET_ORDER)? 63 | ('['.implode(',', 64 | array_map(function($pair) { 65 | return '\''.$pair[1].'\'=>'. 66 | (preg_match('/^\'.*\'$/',$pair[2]) || 67 | preg_match('/\$/',$pair[2])? 68 | $pair[2]: 69 | \Base::instance()->stringify($pair[2])); 70 | },$pairs)).']+get_defined_vars()'): 71 | 'get_defined_vars()'; 72 | $ttl=isset($attrib['ttl'])?(int)$attrib['ttl']:0; 73 | return 74 | 'token($attrib['if']).') '):''). 76 | ('echo $this->render('. 77 | (preg_match('/^\{\{(.+?)\}\}$/',$attrib['href'])? 78 | $this->token($attrib['href']): 79 | Base::instance()->stringify($attrib['href'])).','. 80 | 'NULL,'.$hive.','.$ttl.'); ?>'); 81 | } 82 | 83 | /** 84 | * Template -exclude- tag handler 85 | * @return string 86 | **/ 87 | protected function _exclude() { 88 | return ''; 89 | } 90 | 91 | /** 92 | * Template -ignore- tag handler 93 | * @return string 94 | * @param $node array 95 | **/ 96 | protected function _ignore(array $node) { 97 | return $node[0]; 98 | } 99 | 100 | /** 101 | * Template -loop- tag handler 102 | * @return string 103 | * @param $node array 104 | **/ 105 | protected function _loop(array $node) { 106 | $attrib=$node['@attrib']; 107 | unset($node['@attrib']); 108 | return 109 | 'token($attrib['from']).';'. 111 | $this->token($attrib['to']).';'. 112 | $this->token($attrib['step']).'): ?>'. 113 | $this->build($node). 114 | ''; 115 | } 116 | 117 | /** 118 | * Template -repeat- tag handler 119 | * @return string 120 | * @param $node array 121 | **/ 122 | protected function _repeat(array $node) { 123 | $attrib=$node['@attrib']; 124 | unset($node['@attrib']); 125 | return 126 | 'token($attrib['counter'])).'=0; '):''). 129 | 'foreach (('. 130 | $this->token($attrib['group']).'?:[]) as '. 131 | (isset($attrib['key'])? 132 | ($this->token($attrib['key']).'=>'):''). 133 | $this->token($attrib['value']).'):'. 134 | (isset($ctr)?(' '.$ctr.'++;'):'').' ?>'. 135 | $this->build($node). 136 | ''; 137 | } 138 | 139 | /** 140 | * Template -check- tag handler 141 | * @return string 142 | * @param $node array 143 | **/ 144 | protected function _check(array $node) { 145 | $attrib=$node['@attrib']; 146 | unset($node['@attrib']); 147 | // Grab and blocks 148 | foreach ($node as $pos=>$block) 149 | if (isset($block['true'])) 150 | $true=[$pos,$block]; 151 | elseif (isset($block['false'])) 152 | $false=[$pos,$block]; 153 | if (isset($true,$false) && $true[0]>$false[0]) 154 | // Reverse and blocks 155 | list($node[$true[0]],$node[$false[0]])=[$false[1],$true[1]]; 156 | return 157 | 'token($attrib['if']).'): ?>'. 158 | $this->build($node). 159 | ''; 160 | } 161 | 162 | /** 163 | * Template -true- tag handler 164 | * @return string 165 | * @param $node array 166 | **/ 167 | protected function _true(array $node) { 168 | return $this->build($node); 169 | } 170 | 171 | /** 172 | * Template -false- tag handler 173 | * @return string 174 | * @param $node array 175 | **/ 176 | protected function _false(array $node) { 177 | return ''.$this->build($node); 178 | } 179 | 180 | /** 181 | * Template -switch- tag handler 182 | * @return string 183 | * @param $node array 184 | **/ 185 | protected function _switch(array $node) { 186 | $attrib=$node['@attrib']; 187 | unset($node['@attrib']); 188 | foreach ($node as $pos=>$block) 189 | if (is_string($block) && !preg_replace('/\s+/','',$block)) 190 | unset($node[$pos]); 191 | return 192 | 'token($attrib['expr']).'): ?>'. 193 | $this->build($node). 194 | ''; 195 | } 196 | 197 | /** 198 | * Template -case- tag handler 199 | * @return string 200 | * @param $node array 201 | **/ 202 | protected function _case(array $node) { 203 | $attrib=$node['@attrib']; 204 | unset($node['@attrib']); 205 | return 206 | 'token($attrib['value']): 208 | Base::instance()->stringify($attrib['value'])).': ?>'. 209 | $this->build($node). 210 | 'token($attrib['break']).') ':''). 212 | 'break; ?>'; 213 | } 214 | 215 | /** 216 | * Template -default- tag handler 217 | * @return string 218 | * @param $node array 219 | **/ 220 | protected function _default(array $node) { 221 | return 222 | ''. 223 | $this->build($node). 224 | ''; 225 | } 226 | 227 | /** 228 | * Assemble markup 229 | * @return string 230 | * @param $node array|string 231 | **/ 232 | function build($node) { 233 | if (is_string($node)) 234 | return parent::build($node); 235 | $out=''; 236 | foreach ($node as $key=>$val) 237 | $out.=is_int($key)?$this->build($val):$this->{'_'.$key}($val); 238 | return $out; 239 | } 240 | 241 | /** 242 | * Extend template with custom tag 243 | * @return NULL 244 | * @param $tag string 245 | * @param $func callback 246 | **/ 247 | function extend($tag,$func) { 248 | $this->tags.='|'.$tag; 249 | $this->custom['_'.$tag]=$func; 250 | } 251 | 252 | /** 253 | * Call custom tag handler 254 | * @return string|FALSE 255 | * @param $func string 256 | * @param $args array 257 | **/ 258 | function __call($func,array $args) { 259 | if ($func[0]=='_') 260 | return call_user_func_array($this->custom[$func],$args); 261 | if (method_exists($this,$func)) 262 | return call_user_func_array([$this,$func],$args); 263 | user_error(sprintf(self::E_Method,$func),E_USER_ERROR); 264 | } 265 | 266 | /** 267 | * Parse string for template directives and tokens 268 | * @return array 269 | * @param $text string 270 | **/ 271 | function parse($text) { 272 | $text=parent::parse($text); 273 | // Build tree structure 274 | for ($ptr=0,$w=5,$len=strlen($text),$tree=[],$tmp='';$ptr<$len;) 275 | if (preg_match('/^(.{0,'.$w.'}?)<(\/?)(?:F3:)?'. 276 | '('.$this->tags.')\b((?:\s+[\w-]+'. 277 | '(?:\h*=\h*(?:"(?:.*?)"|\'(?:.*?)\'))?|'. 278 | '\h*\{\{.+?\}\})*)\h*(\/?)>/is', 279 | substr($text,$ptr),$match)) { 280 | if (strlen($tmp) || $match[1]) 281 | $tree[]=$tmp.$match[1]; 282 | // Element node 283 | if ($match[2]) { 284 | // Find matching start tag 285 | $stack=[]; 286 | for($i=count($tree)-1;$i>=0;$i--) { 287 | $item=$tree[$i]; 288 | if (is_array($item) && 289 | array_key_exists($match[3],$item) && 290 | !isset($item[$match[3]][0])) { 291 | // Start tag found 292 | $tree[$i][$match[3]]+=array_reverse($stack); 293 | $tree=array_slice($tree,0,$i+1); 294 | break; 295 | } 296 | else $stack[]=$item; 297 | } 298 | } 299 | else { 300 | // Start tag 301 | $node=&$tree[][$match[3]]; 302 | $node=[]; 303 | if ($match[4]) { 304 | // Process attributes 305 | preg_match_all( 306 | '/(?:\b([\w-]+)\h*'. 307 | '(?:=\h*(?:"(.*?)"|\'(.*?)\'))?|'. 308 | '(\{\{.+?\}\}))/s', 309 | $match[4],$attr,PREG_SET_ORDER); 310 | foreach ($attr as $kv) 311 | if (isset($kv[4])) 312 | $node['@attrib'][]=$kv[4]; 313 | else 314 | $node['@attrib'][$kv[1]]= 315 | (isset($kv[2]) && $kv[2]!==''? 316 | $kv[2]: 317 | (isset($kv[3]) && $kv[3]!==''? 318 | $kv[3]:NULL)); 319 | } 320 | } 321 | $tmp=''; 322 | $ptr+=strlen($match[0]); 323 | $w=5; 324 | } 325 | else { 326 | // Text node 327 | $tmp.=substr($text,$ptr,$w); 328 | $ptr+=$w; 329 | if ($w<50) 330 | $w++; 331 | } 332 | if (strlen($tmp)) 333 | // Append trailing text 334 | $tree[]=$tmp; 335 | // Break references 336 | unset($node); 337 | return $tree; 338 | } 339 | 340 | /** 341 | * Class constructor 342 | * return object 343 | **/ 344 | function __construct() { 345 | $ref=new ReflectionClass(__CLASS__); 346 | $this->tags=''; 347 | foreach ($ref->getmethods() as $method) 348 | if (preg_match('/^_(?=[[:alpha:]])/',$method->name)) 349 | $this->tags.=(strlen($this->tags)?'|':''). 350 | substr($method->name,1); 351 | } 352 | 353 | } 354 | -------------------------------------------------------------------------------- /sample-application/lib/smtp.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | //! SMTP plug-in 24 | class SMTP extends Magic { 25 | 26 | //@{ Locale-specific error/exception messages 27 | const 28 | E_Header='%s: header is required', 29 | E_Blank='Message must not be blank', 30 | E_Attach='Attachment %s not found'; 31 | //@} 32 | 33 | protected 34 | //! Message properties 35 | $headers, 36 | //! E-mail attachments 37 | $attachments, 38 | //! SMTP host 39 | $host, 40 | //! SMTP port 41 | $port, 42 | //! TLS/SSL 43 | $scheme, 44 | //! User ID 45 | $user, 46 | //! Password 47 | $pw, 48 | //! TLS/SSL stream context 49 | $context, 50 | //! TCP/IP socket 51 | $socket, 52 | //! Server-client conversation 53 | $log; 54 | 55 | /** 56 | * Fix header 57 | * @return string 58 | * @param $key string 59 | **/ 60 | protected function fixheader($key) { 61 | return str_replace(' ','-', 62 | ucwords(preg_replace('/[_-]/',' ',strtolower($key)))); 63 | } 64 | 65 | /** 66 | * Return TRUE if header exists 67 | * @return bool 68 | * @param $key 69 | **/ 70 | function exists($key) { 71 | $key=$this->fixheader($key); 72 | return isset($this->headers[$key]); 73 | } 74 | 75 | /** 76 | * Bind value to e-mail header 77 | * @return string 78 | * @param $key string 79 | * @param $val string 80 | **/ 81 | function set($key,$val) { 82 | $key=$this->fixheader($key); 83 | return $this->headers[$key]=$val; 84 | } 85 | 86 | /** 87 | * Return value of e-mail header 88 | * @return string|NULL 89 | * @param $key string 90 | **/ 91 | function &get($key) { 92 | $key=$this->fixheader($key); 93 | if (isset($this->headers[$key])) 94 | $val=&$this->headers[$key]; 95 | else 96 | $val=NULL; 97 | return $val; 98 | } 99 | 100 | /** 101 | * Remove header 102 | * @return NULL 103 | * @param $key string 104 | **/ 105 | function clear($key) { 106 | $key=$this->fixheader($key); 107 | unset($this->headers[$key]); 108 | } 109 | 110 | /** 111 | * Return client-server conversation history 112 | * @return string 113 | **/ 114 | function log() { 115 | return str_replace("\n",PHP_EOL,$this->log); 116 | } 117 | 118 | /** 119 | * Send SMTP command and record server response 120 | * @return string 121 | * @param $cmd string 122 | * @param $log bool|string 123 | * @param $mock bool 124 | **/ 125 | protected function dialog($cmd=NULL,$log=TRUE,$mock=FALSE) { 126 | $reply=''; 127 | if ($mock) { 128 | $host=str_replace('ssl://','',$this->host); 129 | switch ($cmd) { 130 | case NULL: 131 | $reply='220 '.$host.' ESMTP ready'."\n"; 132 | break; 133 | case 'DATA': 134 | $reply='354 Go ahead'."\n"; 135 | break; 136 | case 'QUIT': 137 | $reply='221 '.$host.' closing connection'."\n"; 138 | break; 139 | default: 140 | $reply='250 OK'."\n"; 141 | break; 142 | } 143 | } 144 | else { 145 | $socket=&$this->socket; 146 | if ($cmd) 147 | fputs($socket,$cmd."\r\n"); 148 | while (!feof($socket) && ($info=stream_get_meta_data($socket)) && 149 | !$info['timed_out'] && $str=fgets($socket,4096)) { 150 | $reply.=$str; 151 | if (preg_match('/(?:^|\n)\d{3} .+?\r\n/s',$reply)) 152 | break; 153 | } 154 | } 155 | if ($log) { 156 | if ($cmd) 157 | $this->log.=$cmd."\n"; 158 | $this->log.=str_replace("\r",'',$reply); 159 | } 160 | return $reply; 161 | } 162 | 163 | /** 164 | * Add e-mail attachment 165 | * @return NULL 166 | * @param $file string 167 | * @param $alias string 168 | * @param $cid string 169 | **/ 170 | function attach($file,$alias=NULL,$cid=NULL) { 171 | if (!is_file($file)) 172 | user_error(sprintf(self::E_Attach,$file),E_USER_ERROR); 173 | if ($alias) 174 | $file=[$alias=>$file]; 175 | $this->attachments[]=['filename'=>$file,'cid'=>$cid]; 176 | } 177 | 178 | /** 179 | * Transmit message 180 | * @return bool 181 | * @param $message string 182 | * @param $log bool|string 183 | * @param $mock bool 184 | **/ 185 | function send($message,$log=TRUE,$mock=FALSE) { 186 | if ($this->scheme=='ssl' && !extension_loaded('openssl')) 187 | return FALSE; 188 | // Message should not be blank 189 | if (!$message) 190 | user_error(self::E_Blank,E_USER_ERROR); 191 | $fw=Base::instance(); 192 | // Retrieve headers 193 | $headers=$this->headers; 194 | // Connect to the server 195 | if (!$mock) { 196 | $socket=&$this->socket; 197 | $socket=@stream_socket_client($this->host.':'.$this->port, 198 | $errno,$errstr,ini_get('default_socket_timeout'), 199 | STREAM_CLIENT_CONNECT,$this->context); 200 | if (!$socket) { 201 | $fw->error(500,$errstr); 202 | return FALSE; 203 | } 204 | stream_set_blocking($socket,TRUE); 205 | } 206 | // Get server's initial response 207 | $this->dialog(NULL,TRUE,$mock); 208 | // Announce presence 209 | $reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock); 210 | if (strtolower($this->scheme)=='tls') { 211 | $this->dialog('STARTTLS',$log,$mock); 212 | if (!$mock) 213 | stream_socket_enable_crypto( 214 | $socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT); 215 | $reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock); 216 | } 217 | $message=wordwrap($message,998); 218 | if (preg_match('/8BITMIME/',$reply)) 219 | $headers['Content-Transfer-Encoding']='8bit'; 220 | else { 221 | $headers['Content-Transfer-Encoding']='quoted-printable'; 222 | $message=preg_replace('/^\.(.+)/m', 223 | '..$1',quoted_printable_encode($message)); 224 | } 225 | if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) { 226 | // Authenticate 227 | $this->dialog('AUTH LOGIN',$log,$mock); 228 | $this->dialog(base64_encode($this->user),$log,$mock); 229 | $reply=$this->dialog(base64_encode($this->pw),$log,$mock); 230 | if (!preg_match('/^235\s.*/',$reply)) { 231 | $this->dialog('QUIT',$log,$mock); 232 | if (!$mock && $socket) 233 | fclose($socket); 234 | return FALSE; 235 | } 236 | } 237 | if (empty($headers['Message-Id'])) 238 | $headers['Message-Id']='<'.uniqid('',TRUE).'@'.$this->host.'>'; 239 | if (empty($headers['Date'])) 240 | $headers['Date']=date('r'); 241 | // Required headers 242 | $reqd=['From','To','Subject']; 243 | foreach ($reqd as $id) 244 | if (empty($headers[$id])) 245 | user_error(sprintf(self::E_Header,$id),E_USER_ERROR); 246 | $eol="\r\n"; 247 | $str=''; 248 | // Stringify headers 249 | foreach ($headers as $key=>&$val) { 250 | if (!in_array($key,$reqd) && 251 | (!$this->attachments || 252 | $key!='Content-Type' && 253 | $key!='Content-Transfer-Encoding')) 254 | $str.=$key.': '.$val.$eol; 255 | if (in_array($key,['From','To','Cc','Bcc'])) { 256 | $email=''; 257 | preg_match_all('/(?:".+?" )?(?:<.+?>|[^ ,]+)/', 258 | $val,$matches,PREG_SET_ORDER); 259 | foreach ($matches as $raw) 260 | $email.=($email?', ':''). 261 | (preg_match('/<.+?>/',$raw[0])? 262 | $raw[0]: 263 | ('<'.$raw[0].'>')); 264 | $val=$email; 265 | } 266 | unset($val); 267 | } 268 | // Start message dialog 269 | $this->dialog('MAIL FROM: '.strstr($headers['From'],'<'),$log,$mock); 270 | foreach ($fw->split($headers['To']. 271 | (isset($headers['Cc'])?(';'.$headers['Cc']):''). 272 | (isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst) { 273 | $this->dialog('RCPT TO: '.strstr($dst,'<'),$log,$mock); 274 | } 275 | $this->dialog('DATA',$log,$mock); 276 | if ($this->attachments) { 277 | // Replace Content-Type 278 | $type=$headers['Content-Type']; 279 | unset($headers['Content-Type']); 280 | $enc=$headers['Content-Transfer-Encoding']; 281 | unset($headers['Content-Transfer-Encoding']); 282 | $hash=uniqid(NULL,TRUE); 283 | // Send mail headers 284 | $out='Content-Type: multipart/mixed; boundary="'.$hash.'"'.$eol; 285 | foreach ($headers as $key=>$val) 286 | if ($key!='Bcc') 287 | $out.=$key.': '.$val.$eol; 288 | $out.=$eol; 289 | $out.='This is a multi-part message in MIME format'.$eol; 290 | $out.=$eol; 291 | $out.='--'.$hash.$eol; 292 | $out.='Content-Type: '.$type.$eol; 293 | $out.='Content-Transfer-Encoding: '.$enc.$eol; 294 | $out.=$str.$eol; 295 | $out.=$message.$eol; 296 | foreach ($this->attachments as $attachment) { 297 | if (is_array($attachment['filename'])) 298 | list($alias,$file)=each($attachment['filename']); 299 | else 300 | $alias=basename($file=$attachment['filename']); 301 | $out.='--'.$hash.$eol; 302 | $out.='Content-Type: application/octet-stream'.$eol; 303 | $out.='Content-Transfer-Encoding: base64'.$eol; 304 | if ($attachment['cid']) 305 | $out.='Content-Id: '.$attachment['cid'].$eol; 306 | $out.='Content-Disposition: attachment; '. 307 | 'filename="'.$alias.'"'.$eol; 308 | $out.=$eol; 309 | $out.=chunk_split(base64_encode( 310 | file_get_contents($file))).$eol; 311 | } 312 | $out.=$eol; 313 | $out.='--'.$hash.'--'.$eol; 314 | $out.='.'; 315 | $this->dialog($out,preg_match('/verbose/i',$log),$mock); 316 | } 317 | else { 318 | // Send mail headers 319 | $out=''; 320 | foreach ($headers as $key=>$val) 321 | if ($key!='Bcc') 322 | $out.=$key.': '.$val.$eol; 323 | $out.=$eol; 324 | $out.=$message.$eol; 325 | $out.='.'; 326 | // Send message 327 | $this->dialog($out,preg_match('/verbose/i',$log),$mock); 328 | } 329 | $this->dialog('QUIT',$log,$mock); 330 | if (!$mock && $socket) 331 | fclose($socket); 332 | return TRUE; 333 | } 334 | 335 | /** 336 | * Instantiate class 337 | * @param $host string 338 | * @param $port int 339 | * @param $scheme string 340 | * @param $user string 341 | * @param $pw string 342 | * @param $ctx resource 343 | **/ 344 | function __construct( 345 | $host='localhost',$port=25,$scheme=NULL,$user=NULL,$pw=NULL,$ctx=NULL) { 346 | $this->headers=[ 347 | 'MIME-Version'=>'1.0', 348 | 'Content-Type'=>'text/plain; '. 349 | 'charset='.Base::instance()->ENCODING 350 | ]; 351 | $this->host=strtolower((($this->scheme=strtolower($scheme))=='ssl'? 352 | 'ssl':'tcp').'://'.$host); 353 | $this->port=$port; 354 | $this->user=$user; 355 | $this->pw=$pw; 356 | $this->context=stream_context_create($ctx); 357 | } 358 | 359 | } 360 | -------------------------------------------------------------------------------- /sample-application/lib/db/mongo/mapper.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace DB\Mongo; 24 | 25 | //! MongoDB mapper 26 | class Mapper extends \DB\Cursor { 27 | 28 | protected 29 | //! MongoDB wrapper 30 | $db, 31 | //! Legacy flag 32 | $legacy, 33 | //! Mongo collection 34 | $collection, 35 | //! Mongo document 36 | $document=[], 37 | //! Mongo cursor 38 | $cursor, 39 | //! Defined fields 40 | $fields; 41 | 42 | /** 43 | * Return database type 44 | * @return string 45 | **/ 46 | function dbtype() { 47 | return 'Mongo'; 48 | } 49 | 50 | /** 51 | * Return TRUE if field is defined 52 | * @return bool 53 | * @param $key string 54 | **/ 55 | function exists($key) { 56 | return array_key_exists($key,$this->document); 57 | } 58 | 59 | /** 60 | * Assign value to field 61 | * @return scalar|FALSE 62 | * @param $key string 63 | * @param $val scalar 64 | **/ 65 | function set($key,$val) { 66 | return $this->document[$key]=$val; 67 | } 68 | 69 | /** 70 | * Retrieve value of field 71 | * @return scalar|FALSE 72 | * @param $key string 73 | **/ 74 | function &get($key) { 75 | if ($this->exists($key)) 76 | return $this->document[$key]; 77 | user_error(sprintf(self::E_Field,$key),E_USER_ERROR); 78 | } 79 | 80 | /** 81 | * Delete field 82 | * @return NULL 83 | * @param $key string 84 | **/ 85 | function clear($key) { 86 | unset($this->document[$key]); 87 | } 88 | 89 | /** 90 | * Convert array to mapper object 91 | * @return static 92 | * @param $row array 93 | **/ 94 | protected function factory($row) { 95 | $mapper=clone($this); 96 | $mapper->reset(); 97 | foreach ($row as $key=>$val) 98 | $mapper->document[$key]=$val; 99 | $mapper->query=[clone($mapper)]; 100 | if (isset($mapper->trigger['load'])) 101 | \Base::instance()->call($mapper->trigger['load'],$mapper); 102 | return $mapper; 103 | } 104 | 105 | /** 106 | * Return fields of mapper object as an associative array 107 | * @return array 108 | * @param $obj object 109 | **/ 110 | function cast($obj=NULL) { 111 | if (!$obj) 112 | $obj=$this; 113 | return $obj->document; 114 | } 115 | 116 | /** 117 | * Build query and execute 118 | * @return static[] 119 | * @param $fields string 120 | * @param $filter array 121 | * @param $options array 122 | * @param $ttl int 123 | **/ 124 | function select($fields=NULL,$filter=NULL,array $options=NULL,$ttl=0) { 125 | if (!$options) 126 | $options=[]; 127 | $options+=[ 128 | 'group'=>NULL, 129 | 'order'=>NULL, 130 | 'limit'=>0, 131 | 'offset'=>0 132 | ]; 133 | $fw=\Base::instance(); 134 | $cache=\Cache::instance(); 135 | if (!($cached=$cache->exists($hash=$fw->hash($this->db->dsn(). 136 | $fw->stringify([$fields,$filter,$options])).'.mongo', 137 | $result)) || !$ttl || $cached[0]+$ttlcollection->group( 140 | $options['group']['keys'], 141 | $options['group']['initial'], 142 | $options['group']['reduce'], 143 | [ 144 | 'condition'=>$filter, 145 | 'finalize'=>$options['group']['finalize'] 146 | ] 147 | ); 148 | $tmp=$this->db->selectcollection( 149 | $fw->HOST.'.'.$fw->BASE.'.'. 150 | uniqid(NULL,TRUE).'.tmp' 151 | ); 152 | $tmp->batchinsert($grp['retval'],['w'=>1]); 153 | $filter=[]; 154 | $collection=$tmp; 155 | } 156 | else { 157 | $filter=$filter?:[]; 158 | $collection=$this->collection; 159 | } 160 | if ($this->legacy) { 161 | $this->cursor=$collection->find($filter,$fields?:[]); 162 | if ($options['order']) 163 | $this->cursor=$this->cursor->sort($options['order']); 164 | if ($options['limit']) 165 | $this->cursor=$this->cursor->limit($options['limit']); 166 | if ($options['offset']) 167 | $this->cursor=$this->cursor->skip($options['offset']); 168 | $result=[]; 169 | while ($this->cursor->hasnext()) 170 | $result[]=$this->cursor->getnext(); 171 | } 172 | else { 173 | $this->cursor=$collection->find($filter,[ 174 | 'sort'=>$options['order'], 175 | 'limit'=>$options['limit'], 176 | 'skip'=>$options['offset'] 177 | ]); 178 | $result=$this->cursor->toarray(); 179 | } 180 | if ($options['group']) 181 | $tmp->drop(); 182 | if ($fw->CACHE && $ttl) 183 | // Save to cache backend 184 | $cache->set($hash,$result,$ttl); 185 | } 186 | $out=[]; 187 | foreach ($result as $doc) 188 | $out[]=$this->factory($doc); 189 | return $out; 190 | } 191 | 192 | /** 193 | * Return records that match criteria 194 | * @return static[] 195 | * @param $filter array 196 | * @param $options array 197 | * @param $ttl int 198 | **/ 199 | function find($filter=NULL,array $options=NULL,$ttl=0) { 200 | if (!$options) 201 | $options=[]; 202 | $options+=[ 203 | 'group'=>NULL, 204 | 'order'=>NULL, 205 | 'limit'=>0, 206 | 'offset'=>0 207 | ]; 208 | return $this->select($this->fields,$filter,$options,$ttl); 209 | } 210 | 211 | /** 212 | * Count records that match criteria 213 | * @return int 214 | * @param $filter array 215 | * @param $options array 216 | * @param $ttl int 217 | **/ 218 | function count($filter=NULL,array $options=NULL,$ttl=0) { 219 | $fw=\Base::instance(); 220 | $cache=\Cache::instance(); 221 | if (!($cached=$cache->exists($hash=$fw->hash($fw->stringify( 222 | [$filter])).'.mongo',$result)) || !$ttl || 223 | $cached[0]+$ttlcollection->count($filter?:[]); 225 | if ($fw->CACHE && $ttl) 226 | // Save to cache backend 227 | $cache->set($hash,$result,$ttl); 228 | } 229 | return $result; 230 | } 231 | 232 | /** 233 | * Return record at specified offset using criteria of previous 234 | * load() call and make it active 235 | * @return array 236 | * @param $ofs int 237 | **/ 238 | function skip($ofs=1) { 239 | $this->document=($out=parent::skip($ofs))?$out->document:[]; 240 | if ($this->document && isset($this->trigger['load'])) 241 | \Base::instance()->call($this->trigger['load'],$this); 242 | return $out; 243 | } 244 | 245 | /** 246 | * Insert new record 247 | * @return array 248 | **/ 249 | function insert() { 250 | if (isset($this->document['_id'])) 251 | return $this->update(); 252 | if (isset($this->trigger['beforeinsert']) && 253 | \Base::instance()->call($this->trigger['beforeinsert'], 254 | [$this,['_id'=>$this->document['_id']]])===FALSE) 255 | return $this->document; 256 | if ($this->legacy) { 257 | $this->collection->insert($this->document); 258 | $pkey=['_id'=>$this->document['_id']]; 259 | } 260 | else { 261 | $result=$this->collection->insertone($this->document); 262 | $pkey=['_id'=>$result->getinsertedid()]; 263 | } 264 | if (isset($this->trigger['afterinsert'])) 265 | \Base::instance()->call($this->trigger['afterinsert'], 266 | [$this,$pkey]); 267 | $this->load($pkey); 268 | return $this->document; 269 | } 270 | 271 | /** 272 | * Update current record 273 | * @return array 274 | **/ 275 | function update() { 276 | $pkey=['_id'=>$this->document['_id']]; 277 | if (isset($this->trigger['beforeupdate']) && 278 | \Base::instance()->call($this->trigger['beforeupdate'], 279 | [$this,$pkey])===FALSE) 280 | return $this->document; 281 | $upsert=['upsert'=>TRUE]; 282 | if ($this->legacy) 283 | $this->collection->update($pkey,$this->document,$upsert); 284 | else 285 | $this->collection->replaceone($pkey,$this->document,$upsert); 286 | if (isset($this->trigger['afterupdate'])) 287 | \Base::instance()->call($this->trigger['afterupdate'], 288 | [$this,$pkey]); 289 | return $this->document; 290 | } 291 | 292 | /** 293 | * Delete current record 294 | * @return bool 295 | * @param $quick bool 296 | * @param $filter array 297 | **/ 298 | function erase($filter=NULL,$quick=TRUE) { 299 | if ($filter) { 300 | if (!$quick) { 301 | foreach ($this->find($filter) as $mapper) 302 | if (!$mapper->erase()) 303 | return FALSE; 304 | return TRUE; 305 | } 306 | return $this->legacy? 307 | $this->collection->remove($filter): 308 | $this->collection->deletemany($filter); 309 | } 310 | $pkey=['_id'=>$this->document['_id']]; 311 | if (isset($this->trigger['beforeerase']) && 312 | \Base::instance()->call($this->trigger['beforeerase'], 313 | [$this,$pkey])===FALSE) 314 | return FALSE; 315 | $result=$this->legacy? 316 | $this->collection->remove(['_id'=>$this->document['_id']]): 317 | $this->collection->deleteone(['_id'=>$this->document['_id']]); 318 | parent::erase(); 319 | if (isset($this->trigger['aftererase'])) 320 | \Base::instance()->call($this->trigger['aftererase'], 321 | [$this,$pkey]); 322 | return $result; 323 | } 324 | 325 | /** 326 | * Reset cursor 327 | * @return NULL 328 | **/ 329 | function reset() { 330 | $this->document=[]; 331 | parent::reset(); 332 | } 333 | 334 | /** 335 | * Hydrate mapper object using hive array variable 336 | * @return NULL 337 | * @param $var array|string 338 | * @param $func callback 339 | **/ 340 | function copyfrom($var,$func=NULL) { 341 | if (is_string($var)) 342 | $var=\Base::instance()->$var; 343 | if ($func) 344 | $var=call_user_func($func,$var); 345 | foreach ($var as $key=>$val) 346 | $this->set($key,$val); 347 | } 348 | 349 | /** 350 | * Populate hive array variable with mapper fields 351 | * @return NULL 352 | * @param $key string 353 | **/ 354 | function copyto($key) { 355 | $var=&\Base::instance()->ref($key); 356 | foreach ($this->document as $key=>$field) 357 | $var[$key]=$field; 358 | } 359 | 360 | /** 361 | * Return field names 362 | * @return array 363 | **/ 364 | function fields() { 365 | return array_keys($this->document); 366 | } 367 | 368 | /** 369 | * Return the cursor from last query 370 | * @return object|NULL 371 | **/ 372 | function cursor() { 373 | return $this->cursor; 374 | } 375 | 376 | /** 377 | * Retrieve external iterator for fields 378 | * @return object 379 | **/ 380 | function getiterator() { 381 | return new \ArrayIterator($this->cast()); 382 | } 383 | 384 | /** 385 | * Instantiate class 386 | * @return void 387 | * @param $db object 388 | * @param $collection string 389 | * @param $fields array 390 | **/ 391 | function __construct(\DB\Mongo $db,$collection,$fields=NULL) { 392 | $this->db=$db; 393 | $this->legacy=$db->legacy(); 394 | $this->collection=$db->selectcollection($collection); 395 | $this->fields=$fields; 396 | $this->reset(); 397 | } 398 | 399 | } 400 | -------------------------------------------------------------------------------- /sample-application/lib/cli/ws.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace CLI; 24 | 25 | //! RFC6455 WebSocket server 26 | class WS { 27 | 28 | const 29 | //! UUID magic string 30 | Magic='258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 31 | //! Max packet size 32 | Packet=65536; 33 | 34 | //@{ Mask bits for first byte of header 35 | const 36 | Text=0x01, 37 | Binary=0x02, 38 | Close=0x08, 39 | Ping=0x09, 40 | Pong=0x0a, 41 | OpCode=0x0f, 42 | Finale=0x80; 43 | //@} 44 | 45 | //@{ Mask bits for second byte of header 46 | const 47 | Length=0x7f; 48 | //@} 49 | 50 | protected 51 | $addr, 52 | $ctx, 53 | $wait, 54 | $sockets, 55 | $agents=[], 56 | $events=[]; 57 | 58 | /** 59 | * Allocate stream socket 60 | * @return NULL 61 | * @param $socket resource 62 | **/ 63 | function alloc($socket) { 64 | if (is_bool($str=$this->read($socket))) { 65 | $this->close($socket); 66 | return; 67 | } 68 | // Get WebSocket headers 69 | $hdrs=[]; 70 | $CRLF="\r\n"; 71 | $verb=NULL; 72 | $uri=NULL; 73 | foreach (explode($CRLF,trim($str)) as $line) 74 | if (preg_match('/^(\w+)\s(.+)\sHTTP\/1\.\d$/', 75 | trim($line),$match)) { 76 | $verb=$match[1]; 77 | $uri=$match[2]; 78 | } 79 | else 80 | if (preg_match('/^(.+): (.+)/',trim($line),$match)) 81 | // Standardize header 82 | $hdrs[ 83 | strtr( 84 | ucwords( 85 | strtolower( 86 | strtr($match[1],'-',' ') 87 | ) 88 | ),' ','-' 89 | ) 90 | ]=$match[2]; 91 | else { 92 | $this->close($socket); 93 | return; 94 | } 95 | if (empty($hdrs['Upgrade']) && 96 | empty($hdrs['Sec-Websocket-Key'])) { 97 | // Not a WebSocket request 98 | if ($verb && $uri) 99 | $this->write( 100 | $socket, 101 | $str='HTTP/1.1 400 Bad Request'.$CRLF. 102 | 'Connection: close'.$CRLF.$CRLF 103 | ); 104 | $this->close($socket); 105 | return; 106 | } 107 | // Handshake 108 | $bytes=$this->write( 109 | $socket, 110 | $str='HTTP/1.1 101 Switching Protocols'.$CRLF. 111 | 'Upgrade: websocket'.$CRLF. 112 | 'Connection: Upgrade'.$CRLF. 113 | 'Sec-WebSocket-Accept: '. 114 | base64_encode( 115 | sha1( 116 | $hdrs['Sec-Websocket-Key']. 117 | self::Magic, 118 | TRUE 119 | ) 120 | ).$CRLF.$CRLF 121 | ); 122 | if ($bytes) { 123 | // Connect agent to server 124 | $this->sockets[]=$socket; 125 | $this->agents[(int)$socket]= 126 | new Agent($this,$socket,$verb,$uri,$hdrs); 127 | } 128 | else 129 | $this->close($socket); 130 | } 131 | 132 | /** 133 | * Close stream socket 134 | * @return NULL 135 | * @param $socket resource 136 | **/ 137 | function close($socket) { 138 | stream_socket_shutdown($socket,STREAM_SHUT_WR); 139 | @fclose($socket); 140 | } 141 | 142 | /** 143 | * Free stream socket 144 | * @return bool 145 | * @param $socket resource 146 | **/ 147 | function free($socket) { 148 | unset($this->sockets[array_search($socket,$this->sockets)]); 149 | unset($this->agents[(int)$socket]); 150 | $this->close($socket); 151 | } 152 | 153 | /** 154 | * Read from stream socket 155 | * @return string|FALSE 156 | * @param $socket resource 157 | **/ 158 | function read($socket) { 159 | if (is_string($str=@fread($socket,self::Packet)) && 160 | strlen($str) && 161 | strlen($str)events['error']) && 164 | is_callable($func=$this->events['error'])) 165 | $func($this); 166 | return FALSE; 167 | } 168 | 169 | /** 170 | * Write to stream socket 171 | * @return int|FALSE 172 | * @param $socket resource 173 | * @param $str string 174 | **/ 175 | function write($socket,$str) { 176 | for ($i=0,$bytes=0;$ievents['error']) && 181 | is_callable($func=$this->events['error'])) 182 | $func($this); 183 | return FALSE; 184 | } 185 | return $bytes; 186 | } 187 | 188 | /** 189 | * Return socket agents 190 | * @return array 191 | * @param $uri string 192 | ***/ 193 | function agents($uri=NULL) { 194 | return array_filter( 195 | $this->agents, 196 | function($val) use($uri) { 197 | return $uri?($val->uri()==$uri):TRUE; 198 | } 199 | ); 200 | } 201 | 202 | /** 203 | * Return event handlers 204 | * @return array 205 | **/ 206 | function events() { 207 | return $this->events; 208 | } 209 | 210 | /** 211 | * Bind function to event handler 212 | * @return object 213 | * @param $event string 214 | * @param $func callable 215 | **/ 216 | function on($event,$func) { 217 | $this->events[$event]=$func; 218 | return $this; 219 | } 220 | 221 | /** 222 | * Terminate server 223 | * @return NULL 224 | * @param $signal int 225 | **/ 226 | function kill($signal) { 227 | die; 228 | } 229 | 230 | /** 231 | * Execute the server process 232 | * @return object 233 | **/ 234 | function run() { 235 | $fw=\Base::instance(); 236 | // Assign signal handlers 237 | declare(ticks=1); 238 | pcntl_signal(SIGINT,[$this,'kill']); 239 | pcntl_signal(SIGTERM,[$this,'kill']); 240 | gc_enable(); 241 | // Activate WebSocket listener 242 | $listen=stream_socket_server( 243 | $this->addr,$errno,$errstr, 244 | STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, 245 | $this->ctx 246 | ); 247 | $socket=socket_import_stream($listen); 248 | register_shutdown_function(function() use($listen) { 249 | foreach ($this->sockets as $socket) 250 | if ($socket!=$listen) 251 | $this->free($socket); 252 | $this->close($listen); 253 | if (isset($this->events['stop']) && 254 | is_callable($func=$this->events['stop'])) 255 | $func($this); 256 | }); 257 | if ($errstr) 258 | user_error($errstr,E_USER_ERROR); 259 | if (isset($this->events['start']) && 260 | is_callable($func=$this->events['start'])) 261 | $func($this); 262 | $this->sockets=[$listen]; 263 | $empty=[]; 264 | $wait=$this->wait; 265 | while (TRUE) { 266 | $active=$this->sockets; 267 | $mark=microtime(TRUE); 268 | $count=@stream_select( 269 | $active,$empty,$empty,(int)$wait,round(1e6*($wait-(int)$wait)) 270 | ); 271 | if (is_bool($count) && $wait) { 272 | if (isset($this->events['error']) && 273 | is_callable($func=$this->events['error'])) 274 | $func($this); 275 | die; 276 | } 277 | if ($count) { 278 | // Process active connections 279 | foreach ($active as $socket) { 280 | if (!is_resource($socket)) 281 | continue; 282 | if ($socket==$listen) { 283 | if ($socket=@stream_socket_accept($listen,0)) 284 | $this->alloc($socket); 285 | else 286 | if (isset($this->events['error']) && 287 | is_callable($func=$this->events['error'])) 288 | $func($this); 289 | } 290 | else { 291 | $id=(int)$socket; 292 | if (isset($this->agents[$id]) && 293 | $raw=$this->agents[$id]->fetch()) { 294 | list($op,$data)=$raw; 295 | // Dispatch 296 | switch ($op & self::OpCode) { 297 | case self::Ping: 298 | $this->agents[$id]->send(self::Pong); 299 | break; 300 | case self::Close: 301 | $this->free($socket); 302 | break; 303 | case self::Text: 304 | $data=trim($data); 305 | case self::Binary: 306 | if (isset($this->events['receive']) && 307 | is_callable($func=$this->events['receive'])) 308 | $func($this->agents[$id],$op,$data); 309 | break; 310 | } 311 | } 312 | } 313 | } 314 | $wait-=microtime(TRUE)-$mark; 315 | while ($wait<1e-6) { 316 | $wait+=$this->wait; 317 | $count=0; 318 | } 319 | } 320 | if (!$count) { 321 | $mark=microtime(TRUE); 322 | foreach ($this->sockets as $socket) { 323 | if (!is_resource($socket)) 324 | continue; 325 | $id=(int)$socket; 326 | if ($socket!=$listen && 327 | isset($this->agents[$id]) && 328 | isset($this->events['idle']) && 329 | is_callable($func=$this->events['idle'])) 330 | $func($this->agents[$id]); 331 | } 332 | $wait=$this->wait-microtime(TRUE)+$mark; 333 | } 334 | gc_collect_cycles(); 335 | } 336 | } 337 | 338 | /** 339 | * Instantiate object 340 | * @return object 341 | * @param $addr string 342 | * @param $ctx resource 343 | * @param $wait int 344 | **/ 345 | function __construct($addr,$ctx=NULL,$wait=60) { 346 | $this->addr=$addr; 347 | $this->ctx=$ctx?:stream_context_create(); 348 | $this->wait=$wait; 349 | $this->events=[]; 350 | } 351 | 352 | } 353 | 354 | //! RFC6455 remote socket 355 | class Agent { 356 | 357 | protected 358 | $server, 359 | $id, 360 | $socket, 361 | $flag, 362 | $verb, 363 | $uri, 364 | $headers, 365 | $events, 366 | $buffer; 367 | 368 | /** 369 | * Return server instance 370 | * @return object 371 | **/ 372 | function server() { 373 | return $this->server; 374 | } 375 | 376 | /** 377 | * Return socket ID 378 | * @return string 379 | **/ 380 | function id() { 381 | return $this->id; 382 | } 383 | 384 | /** 385 | * Return request method 386 | * @return string 387 | **/ 388 | function verb() { 389 | return $this->verb; 390 | } 391 | 392 | /** 393 | * Return request URI 394 | * @return string 395 | **/ 396 | function uri() { 397 | return $this->uri; 398 | } 399 | 400 | /** 401 | * Return socket headers 402 | * @return string 403 | **/ 404 | function headers() { 405 | return $this->headers; 406 | } 407 | 408 | /** 409 | * Frame and transmit payload 410 | * @return string|FALSE 411 | * @param $socket resource 412 | * @param $op int 413 | * @param $payload string 414 | **/ 415 | function send($op,$data='') { 416 | $mask=WS::Finale | $op & WS::OpCode; 417 | $len=strlen($data); 418 | $str=''; 419 | if ($len>0xffff) 420 | $str=pack('CCNN',$mask,0x7f,$len); 421 | else 422 | if ($len>0x7d) 423 | $str=pack('CCn',$mask,0x7e,$len); 424 | else 425 | $str=pack('CC',$mask,$len); 426 | $str.=$data; 427 | $server=$this->server(); 428 | if (is_bool($server->write($this->socket,$str))) { 429 | $this->free(); 430 | return FALSE; 431 | } 432 | if (!in_array($op,[WS::Pong,WS::Close]) && 433 | isset($this->events['send']) && 434 | is_callable($func=$this->events['send'])) 435 | $func($this,$op,$data); 436 | return $data; 437 | } 438 | 439 | /** 440 | * Retrieve and unmask payload 441 | * @return array|FALSE 442 | **/ 443 | function fetch() { 444 | // Unmask payload 445 | $server=$this->server(); 446 | if (is_bool($buf=$server->read($this->socket))) { 447 | $this->free(); 448 | return FALSE; 449 | } 450 | $buf=($this->buffer.=$buf); 451 | $op=ord($buf[0]) & WS::OpCode; 452 | $len=ord($buf[1]) & WS::Length; 453 | $pos=2; 454 | if ($len==0x7e) { 455 | $len=ord($buf[2])*256+ord($buf[3]); 456 | $pos+=2; 457 | } 458 | else 459 | if ($len==0x7f) { 460 | for ($i=0,$len=0;$i<8;$i++) 461 | $len=$len*256+ord($buf[$i+2]); 462 | $pos+=8; 463 | } 464 | for ($i=0,$mask=[];$i<4;$i++) 465 | $mask[$i]=ord($buf[$pos+$i]); 466 | $pos+=4; 467 | if (strlen($buf)<$len+$pos) 468 | return FALSE; 469 | for ($i=0,$data='';$i<$len;$i++) 470 | $data.=chr(ord($buf[$pos+$i])^$mask[$i%4]); 471 | $this->buffer=''; 472 | return [$op,$data]; 473 | } 474 | 475 | /** 476 | * Free stream socket 477 | * @return NULL 478 | **/ 479 | function free() { 480 | $this->server->free($this->socket); 481 | } 482 | 483 | /** 484 | * Destroy object 485 | * @return NULL 486 | **/ 487 | function __destruct() { 488 | if (isset($this->events['disconnect']) && 489 | is_callable($func=$this->events['disconnect'])) 490 | $func($this); 491 | } 492 | 493 | /** 494 | * Instantiate object 495 | * @return object 496 | * @param $server object 497 | * @param $socket resource 498 | * @param $verb string 499 | * @param $uri string 500 | * @param $hdrs array 501 | **/ 502 | function __construct($server,$socket,$verb,$uri,array $hdrs) { 503 | $this->server=$server; 504 | $this->id=stream_socket_get_name($socket,TRUE); 505 | $this->socket=$socket; 506 | $this->verb=$verb; 507 | $this->uri=$uri; 508 | $this->headers=$hdrs; 509 | $this->events=$server->events(); 510 | $this->buffer=''; 511 | if (isset($this->events['connect']) && 512 | is_callable($func=$this->events['connect'])) 513 | $func($this); 514 | } 515 | 516 | } 517 | -------------------------------------------------------------------------------- /sample-application/lib/db/jig/mapper.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace DB\Jig; 24 | 25 | //! Flat-file DB mapper 26 | class Mapper extends \DB\Cursor { 27 | 28 | protected 29 | //! Flat-file DB wrapper 30 | $db, 31 | //! Data file 32 | $file, 33 | //! Document identifier 34 | $id, 35 | //! Document contents 36 | $document=[]; 37 | 38 | /** 39 | * Return database type 40 | * @return string 41 | **/ 42 | function dbtype() { 43 | return 'Jig'; 44 | } 45 | 46 | /** 47 | * Return TRUE if field is defined 48 | * @return bool 49 | * @param $key string 50 | **/ 51 | function exists($key) { 52 | return array_key_exists($key,$this->document); 53 | } 54 | 55 | /** 56 | * Assign value to field 57 | * @return scalar|FALSE 58 | * @param $key string 59 | * @param $val scalar 60 | **/ 61 | function set($key,$val) { 62 | return ($key=='_id')?FALSE:($this->document[$key]=$val); 63 | } 64 | 65 | /** 66 | * Retrieve value of field 67 | * @return scalar|FALSE 68 | * @param $key string 69 | **/ 70 | function &get($key) { 71 | if ($key=='_id') 72 | return $this->id; 73 | if (array_key_exists($key,$this->document)) 74 | return $this->document[$key]; 75 | user_error(sprintf(self::E_Field,$key),E_USER_ERROR); 76 | } 77 | 78 | /** 79 | * Delete field 80 | * @return NULL 81 | * @param $key string 82 | **/ 83 | function clear($key) { 84 | if ($key!='_id') 85 | unset($this->document[$key]); 86 | } 87 | 88 | /** 89 | * Convert array to mapper object 90 | * @return object 91 | * @param $id string 92 | * @param $row array 93 | **/ 94 | protected function factory($id,$row) { 95 | $mapper=clone($this); 96 | $mapper->reset(); 97 | $mapper->id=$id; 98 | foreach ($row as $field=>$val) 99 | $mapper->document[$field]=$val; 100 | $mapper->query=[clone($mapper)]; 101 | if (isset($mapper->trigger['load'])) 102 | \Base::instance()->call($mapper->trigger['load'],$mapper); 103 | return $mapper; 104 | } 105 | 106 | /** 107 | * Return fields of mapper object as an associative array 108 | * @return array 109 | * @param $obj object 110 | **/ 111 | function cast($obj=NULL) { 112 | if (!$obj) 113 | $obj=$this; 114 | return $obj->document+['_id'=>$this->id]; 115 | } 116 | 117 | /** 118 | * Convert tokens in string expression to variable names 119 | * @return string 120 | * @param $str string 121 | **/ 122 | function token($str) { 123 | $str=preg_replace_callback( 124 | '/(?stringify(substr($expr[1],1)): 135 | (preg_match('/^\w+/', 136 | $mix=$this->token($expr[2]))? 137 | $fw->stringify($mix): 138 | $mix)). 139 | ']'; 140 | }, 141 | $token[1] 142 | ); 143 | }, 144 | $str 145 | ); 146 | return trim($str); 147 | } 148 | 149 | /** 150 | * Return records that match criteria 151 | * @return static[]|FALSE 152 | * @param $filter array 153 | * @param $options array 154 | * @param $ttl int 155 | * @param $log bool 156 | **/ 157 | function find($filter=NULL,array $options=NULL,$ttl=0,$log=TRUE) { 158 | if (!$options) 159 | $options=[]; 160 | $options+=[ 161 | 'order'=>NULL, 162 | 'limit'=>0, 163 | 'offset'=>0 164 | ]; 165 | $fw=\Base::instance(); 166 | $cache=\Cache::instance(); 167 | $db=$this->db; 168 | $now=microtime(TRUE); 169 | $data=[]; 170 | if (!$fw->CACHE || !$ttl || !($cached=$cache->exists( 171 | $hash=$fw->hash($this->db->dir(). 172 | $fw->stringify([$filter,$options])).'.jig',$data)) || 173 | $cached[0]+$ttlread($this->file); 175 | if (is_null($data)) 176 | return FALSE; 177 | foreach ($data as $id=>&$doc) { 178 | $doc['_id']=$id; 179 | unset($doc); 180 | } 181 | if ($filter) { 182 | if (!is_array($filter)) 183 | return FALSE; 184 | // Normalize equality operator 185 | $expr=preg_replace('/(?<=[^<>!=])=(?!=)/','==',$filter[0]); 186 | // Prepare query arguments 187 | $args=isset($filter[1]) && is_array($filter[1])? 188 | $filter[1]: 189 | array_slice($filter,1,NULL,TRUE); 190 | $args=is_array($args)?$args:[1=>$args]; 191 | $keys=$vals=[]; 192 | $tokens=array_slice( 193 | token_get_all('token($expr)),1); 194 | $data=array_filter($data, 195 | function($_row) use($fw,$args,$tokens) { 196 | $_expr=''; 197 | $ctr=0; 198 | $named=FALSE; 199 | foreach ($tokens as $token) { 200 | if (is_string($token)) 201 | if ($token=='?') { 202 | // Positional 203 | $ctr++; 204 | $key=$ctr; 205 | } 206 | else { 207 | if ($token==':') 208 | $named=TRUE; 209 | else 210 | $_expr.=$token; 211 | continue; 212 | } 213 | elseif ($named && 214 | token_name($token[0])=='T_STRING') { 215 | $key=':'.$token[1]; 216 | $named=FALSE; 217 | } 218 | else { 219 | $_expr.=$token[1]; 220 | continue; 221 | } 222 | $_expr.=$fw->stringify( 223 | is_string($args[$key])? 224 | addcslashes($args[$key],'\''): 225 | $args[$key]); 226 | } 227 | // Avoid conflict with user code 228 | unset($fw,$tokens,$args,$ctr,$token,$key,$named); 229 | extract($_row); 230 | // Evaluate pseudo-SQL expression 231 | return eval('return '.$_expr.';'); 232 | } 233 | ); 234 | } 235 | if (isset($options['order'])) { 236 | $cols=$fw->split($options['order']); 237 | uasort( 238 | $data, 239 | function($val1,$val2) use($cols) { 240 | foreach ($cols as $col) { 241 | $parts=explode(' ',$col,2); 242 | $order=empty($parts[1])? 243 | SORT_ASC: 244 | constant($parts[1]); 245 | $col=$parts[0]; 246 | if (!array_key_exists($col,$val1)) 247 | $val1[$col]=NULL; 248 | if (!array_key_exists($col,$val2)) 249 | $val2[$col]=NULL; 250 | list($v1,$v2)=[$val1[$col],$val2[$col]]; 251 | if ($out=strnatcmp($v1,$v2)* 252 | (($order==SORT_ASC)*2-1)) 253 | return $out; 254 | } 255 | return 0; 256 | } 257 | ); 258 | } 259 | $data=array_slice($data, 260 | $options['offset'],$options['limit']?:NULL,TRUE); 261 | if ($fw->CACHE && $ttl) 262 | // Save to cache backend 263 | $cache->set($hash,$data,$ttl); 264 | } 265 | $out=[]; 266 | foreach ($data as $id=>&$doc) { 267 | unset($doc['_id']); 268 | $out[]=$this->factory($id,$doc); 269 | unset($doc); 270 | } 271 | if ($log && isset($args)) { 272 | if ($filter) 273 | foreach ($args as $key=>$val) { 274 | $vals[]=$fw->stringify(is_array($val)?$val[0]:$val); 275 | $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/'; 276 | } 277 | $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. 278 | $this->file.' [find] '. 279 | ($filter?preg_replace($keys,$vals,$filter[0],1):'')); 280 | } 281 | return $out; 282 | } 283 | 284 | /** 285 | * Count records that match criteria 286 | * @return int 287 | * @param $filter array 288 | * @param $options array 289 | * @param $ttl int 290 | **/ 291 | function count($filter=NULL,array $options=NULL,$ttl=0) { 292 | $now=microtime(TRUE); 293 | $out=count($this->find($filter,$options,$ttl,FALSE)); 294 | $this->db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. 295 | $this->file.' [count] '.($filter?json_encode($filter):'')); 296 | return $out; 297 | } 298 | 299 | /** 300 | * Return record at specified offset using criteria of previous 301 | * load() call and make it active 302 | * @return array 303 | * @param $ofs int 304 | **/ 305 | function skip($ofs=1) { 306 | $this->document=($out=parent::skip($ofs))?$out->document:[]; 307 | $this->id=$out?$out->id:NULL; 308 | if ($this->document && isset($this->trigger['load'])) 309 | \Base::instance()->call($this->trigger['load'],$this); 310 | return $out; 311 | } 312 | 313 | /** 314 | * Insert new record 315 | * @return array 316 | **/ 317 | function insert() { 318 | if ($this->id) 319 | return $this->update(); 320 | $db=$this->db; 321 | $now=microtime(TRUE); 322 | while (($id=uniqid(NULL,TRUE)) && 323 | ($data=&$db->read($this->file)) && isset($data[$id]) && 324 | !connection_aborted()) 325 | usleep(mt_rand(0,100)); 326 | $this->id=$id; 327 | $pkey=['_id'=>$this->id]; 328 | if (isset($this->trigger['beforeinsert']) && 329 | \Base::instance()->call($this->trigger['beforeinsert'], 330 | [$this,$pkey])===FALSE) 331 | return $this->document; 332 | $data[$id]=$this->document; 333 | $db->write($this->file,$data); 334 | $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. 335 | $this->file.' [insert] '.json_encode($this->document)); 336 | if (isset($this->trigger['afterinsert'])) 337 | \Base::instance()->call($this->trigger['afterinsert'], 338 | [$this,$pkey]); 339 | $this->load(['@_id=?',$this->id]); 340 | return $this->document; 341 | } 342 | 343 | /** 344 | * Update current record 345 | * @return array 346 | **/ 347 | function update() { 348 | $db=$this->db; 349 | $now=microtime(TRUE); 350 | $data=&$db->read($this->file); 351 | if (isset($this->trigger['beforeupdate']) && 352 | \Base::instance()->call($this->trigger['beforeupdate'], 353 | [$this,['_id'=>$this->id]])===FALSE) 354 | return $this->document; 355 | $data[$this->id]=$this->document; 356 | $db->write($this->file,$data); 357 | $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. 358 | $this->file.' [update] '.json_encode($this->document)); 359 | if (isset($this->trigger['afterupdate'])) 360 | \Base::instance()->call($this->trigger['afterupdate'], 361 | [$this,['_id'=>$this->id]]); 362 | return $this->document; 363 | } 364 | 365 | /** 366 | * Delete current record 367 | * @return bool 368 | * @param $filter array 369 | * @param $quick bool 370 | **/ 371 | function erase($filter=NULL,$quick=FALSE) { 372 | $db=$this->db; 373 | $now=microtime(TRUE); 374 | $data=&$db->read($this->file); 375 | $pkey=['_id'=>$this->id]; 376 | if ($filter) { 377 | foreach ($this->find($filter,NULL,FALSE) as $mapper) 378 | if (!$mapper->erase(null,$quick)) 379 | return FALSE; 380 | return TRUE; 381 | } 382 | elseif (isset($this->id)) { 383 | unset($data[$this->id]); 384 | parent::erase(); 385 | } 386 | else 387 | return FALSE; 388 | if (!$quick && isset($this->trigger['beforeerase']) && 389 | \Base::instance()->call($this->trigger['beforeerase'], 390 | [$this,$pkey])===FALSE) 391 | return FALSE; 392 | $db->write($this->file,$data); 393 | if ($filter) { 394 | $args=isset($filter[1]) && is_array($filter[1])? 395 | $filter[1]: 396 | array_slice($filter,1,NULL,TRUE); 397 | $args=is_array($args)?$args:[1=>$args]; 398 | foreach ($args as $key=>$val) { 399 | $vals[]=\Base::instance()-> 400 | stringify(is_array($val)?$val[0]:$val); 401 | $keys[]='/'.(is_numeric($key)?'\?':preg_quote($key)).'/'; 402 | } 403 | } 404 | $db->jot('('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. 405 | $this->file.' [erase] '. 406 | ($filter?preg_replace($keys,$vals,$filter[0],1):'')); 407 | if (!$quick && isset($this->trigger['aftererase'])) 408 | \Base::instance()->call($this->trigger['aftererase'], 409 | [$this,$pkey]); 410 | return TRUE; 411 | } 412 | 413 | /** 414 | * Reset cursor 415 | * @return NULL 416 | **/ 417 | function reset() { 418 | $this->id=NULL; 419 | $this->document=[]; 420 | parent::reset(); 421 | } 422 | 423 | /** 424 | * Hydrate mapper object using hive array variable 425 | * @return NULL 426 | * @param $var array|string 427 | * @param $func callback 428 | **/ 429 | function copyfrom($var,$func=NULL) { 430 | if (is_string($var)) 431 | $var=\Base::instance()->$var; 432 | if ($func) 433 | $var=call_user_func($func,$var); 434 | foreach ($var as $key=>$val) 435 | $this->set($key,$val); 436 | } 437 | 438 | /** 439 | * Populate hive array variable with mapper fields 440 | * @return NULL 441 | * @param $key string 442 | **/ 443 | function copyto($key) { 444 | $var=&\Base::instance()->ref($key); 445 | foreach ($this->document as $key=>$field) 446 | $var[$key]=$field; 447 | } 448 | 449 | /** 450 | * Return field names 451 | * @return array 452 | **/ 453 | function fields() { 454 | return array_keys($this->document); 455 | } 456 | 457 | /** 458 | * Retrieve external iterator for fields 459 | * @return object 460 | **/ 461 | function getiterator() { 462 | return new \ArrayIterator($this->cast()); 463 | } 464 | 465 | /** 466 | * Instantiate class 467 | * @return void 468 | * @param $db object 469 | * @param $file string 470 | **/ 471 | function __construct(\DB\Jig $db,$file) { 472 | $this->db=$db; 473 | $this->file=$file; 474 | $this->reset(); 475 | } 476 | 477 | } 478 | -------------------------------------------------------------------------------- /sample-application/lib/db/sql.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | namespace DB; 24 | 25 | //! PDO wrapper 26 | class SQL { 27 | 28 | //@{ Error messages 29 | const 30 | E_PKey='Table %s does not have a primary key'; 31 | //@} 32 | 33 | const 34 | PARAM_FLOAT='float'; 35 | 36 | protected 37 | //! UUID 38 | $uuid, 39 | //! Raw PDO 40 | $pdo, 41 | //! Data source name 42 | $dsn, 43 | //! Database engine 44 | $engine, 45 | //! Database name 46 | $dbname, 47 | //! Transaction flag 48 | $trans=FALSE, 49 | //! Number of rows affected by query 50 | $rows=0, 51 | //! SQL log 52 | $log; 53 | 54 | /** 55 | * Begin SQL transaction 56 | * @return bool 57 | **/ 58 | function begin() { 59 | $out=$this->pdo->begintransaction(); 60 | $this->trans=TRUE; 61 | return $out; 62 | } 63 | 64 | /** 65 | * Rollback SQL transaction 66 | * @return bool 67 | **/ 68 | function rollback() { 69 | $out=$this->pdo->rollback(); 70 | $this->trans=FALSE; 71 | return $out; 72 | } 73 | 74 | /** 75 | * Commit SQL transaction 76 | * @return bool 77 | **/ 78 | function commit() { 79 | $out=$this->pdo->commit(); 80 | $this->trans=FALSE; 81 | return $out; 82 | } 83 | 84 | /** 85 | * Return transaction flag 86 | * @return bool 87 | **/ 88 | function trans() { 89 | return $this->trans; 90 | } 91 | 92 | /** 93 | * Map data type of argument to a PDO constant 94 | * @return int 95 | * @param $val scalar 96 | **/ 97 | function type($val) { 98 | switch (gettype($val)) { 99 | case 'NULL': 100 | return \PDO::PARAM_NULL; 101 | case 'boolean': 102 | return \PDO::PARAM_BOOL; 103 | case 'integer': 104 | return \PDO::PARAM_INT; 105 | case 'resource': 106 | return \PDO::PARAM_LOB; 107 | case 'float': 108 | return self::PARAM_FLOAT; 109 | default: 110 | return \PDO::PARAM_STR; 111 | } 112 | } 113 | 114 | /** 115 | * Cast value to PHP type 116 | * @return mixed 117 | * @param $type string 118 | * @param $val mixed 119 | **/ 120 | function value($type,$val) { 121 | switch ($type) { 122 | case self::PARAM_FLOAT: 123 | if (!is_string($val)) 124 | $val=str_replace(',','.',$val); 125 | return $val; 126 | case \PDO::PARAM_NULL: 127 | return (unset)$val; 128 | case \PDO::PARAM_INT: 129 | return (int)$val; 130 | case \PDO::PARAM_BOOL: 131 | return (bool)$val; 132 | case \PDO::PARAM_STR: 133 | return (string)$val; 134 | case \PDO::PARAM_LOB: 135 | return (binary)$val; 136 | } 137 | } 138 | 139 | /** 140 | * Execute SQL statement(s) 141 | * @return array|int|FALSE 142 | * @param $cmds string|array 143 | * @param $args string|array 144 | * @param $ttl int|array 145 | * @param $log bool 146 | * @param $stamp bool 147 | **/ 148 | function exec($cmds,$args=NULL,$ttl=0,$log=TRUE,$stamp=FALSE) { 149 | $tag=''; 150 | if (is_array($ttl)) 151 | list($ttl,$tag)=$ttl; 152 | $auto=FALSE; 153 | if (is_null($args)) 154 | $args=[]; 155 | elseif (is_scalar($args)) 156 | $args=[1=>$args]; 157 | if (is_array($cmds)) { 158 | if (count($args)<($count=count($cmds))) 159 | // Apply arguments to SQL commands 160 | $args=array_fill(0,$count,$args); 161 | if (!$this->trans) { 162 | $this->begin(); 163 | $auto=TRUE; 164 | } 165 | } 166 | else { 167 | $count=1; 168 | $cmds=[$cmds]; 169 | $args=[$args]; 170 | } 171 | if ($this->log===FALSE) 172 | $log=FALSE; 173 | $fw=\Base::instance(); 174 | $cache=\Cache::instance(); 175 | $result=FALSE; 176 | for ($i=0;$i<$count;$i++) { 177 | $cmd=$cmds[$i]; 178 | $arg=$args[$i]; 179 | // ensure 1-based arguments 180 | if (array_key_exists(0,$arg)) { 181 | array_unshift($arg,''); 182 | unset($arg[0]); 183 | } 184 | if (!preg_replace('/(^\s+|[\s;]+$)/','',$cmd)) 185 | continue; 186 | $now=microtime(TRUE); 187 | $keys=$vals=[]; 188 | if ($fw->CACHE && $ttl && ($cached=$cache->exists( 189 | $hash=$fw->hash($this->dsn.$cmd. 190 | $fw->stringify($arg)).($tag?'.'.$tag:'').'.sql',$result)) && 191 | $cached[0]+$ttl>microtime(TRUE)) { 192 | foreach ($arg as $key=>$val) { 193 | $vals[]=$fw->stringify(is_array($val)?$val[0]:$val); 194 | $keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). 195 | '/'; 196 | } 197 | if ($log) 198 | $this->log.=($stamp?(date('r').' '):'').'('. 199 | sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms) '. 200 | '[CACHED] '. 201 | preg_replace($keys,$vals, 202 | str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; 203 | } 204 | elseif (is_object($query=$this->pdo->prepare($cmd))) { 205 | foreach ($arg as $key=>$val) { 206 | if (is_array($val)) { 207 | // User-specified data type 208 | $query->bindvalue($key,$val[0], 209 | $val[1]==self::PARAM_FLOAT?\PDO::PARAM_STR:$val[1]); 210 | $vals[]=$fw->stringify($this->value($val[1],$val[0])); 211 | } 212 | else { 213 | // Convert to PDO data type 214 | $query->bindvalue($key,$val, 215 | ($type=$this->type($val))==self::PARAM_FLOAT? 216 | \PDO::PARAM_STR:$type); 217 | $vals[]=$fw->stringify($this->value($type,$val)); 218 | } 219 | $keys[]='/'.preg_quote(is_numeric($key)?chr(0).'?':$key). 220 | '/'; 221 | } 222 | if ($log) 223 | $this->log.=($stamp?(date('r').' '):'').' (-0ms) '. 224 | preg_replace($keys,$vals, 225 | str_replace('?',chr(0).'?',$cmd),1).PHP_EOL; 226 | $query->execute(); 227 | if ($log) 228 | $this->log=str_replace('(-0ms)', 229 | '('.sprintf('%.1f',1e3*(microtime(TRUE)-$now)).'ms)', 230 | $this->log); 231 | $error=$query->errorinfo(); 232 | if ($error[0]!=\PDO::ERR_NONE) { 233 | // Statement-level error occurred 234 | if ($this->trans) 235 | $this->rollback(); 236 | user_error('PDOStatement: '.$error[2],E_USER_ERROR); 237 | } 238 | if (preg_match('/(?:^[\s\(]*'. 239 | '(?:EXPLAIN|SELECT|PRAGMA|SHOW)|RETURNING)\b/is',$cmd) || 240 | (preg_match('/^\s*(?:CALL|EXEC)\b/is',$cmd) && 241 | $query->columnCount())) { 242 | $result=$query->fetchall(\PDO::FETCH_ASSOC); 243 | // Work around SQLite quote bug 244 | if (preg_match('/sqlite2?/',$this->engine)) 245 | foreach ($result as $pos=>$rec) { 246 | unset($result[$pos]); 247 | $result[$pos]=[]; 248 | foreach ($rec as $key=>$val) 249 | $result[$pos][trim($key,'\'"[]`')]=$val; 250 | } 251 | $this->rows=count($result); 252 | if ($fw->CACHE && $ttl) 253 | // Save to cache backend 254 | $cache->set($hash,$result,$ttl); 255 | } 256 | else 257 | $this->rows=$result=$query->rowcount(); 258 | $query->closecursor(); 259 | unset($query); 260 | } 261 | else { 262 | $error=$this->errorinfo(); 263 | if ($error[0]!=\PDO::ERR_NONE) { 264 | // PDO-level error occurred 265 | if ($this->trans) 266 | $this->rollback(); 267 | user_error('PDO: '.$error[2],E_USER_ERROR); 268 | } 269 | } 270 | } 271 | if ($this->trans && $auto) 272 | $this->commit(); 273 | return $result; 274 | } 275 | 276 | /** 277 | * Return number of rows affected by last query 278 | * @return int 279 | **/ 280 | function count() { 281 | return $this->rows; 282 | } 283 | 284 | /** 285 | * Return SQL profiler results (or disable logging) 286 | * @return string 287 | * @param $flag bool 288 | **/ 289 | function log($flag=TRUE) { 290 | if ($flag) 291 | return $this->log; 292 | $this->log=FALSE; 293 | } 294 | 295 | /** 296 | * Return TRUE if table exists 297 | * @return bool 298 | * @param $table string 299 | **/ 300 | function exists($table) { 301 | $mode=$this->pdo->getAttribute(\PDO::ATTR_ERRMODE); 302 | $this->pdo->setAttribute(\PDO::ATTR_ERRMODE,\PDO::ERRMODE_SILENT); 303 | $out=$this->pdo-> 304 | query('SELECT 1 FROM '.$this->quotekey($table).' LIMIT 1'); 305 | $this->pdo->setAttribute(\PDO::ATTR_ERRMODE,$mode); 306 | return is_object($out); 307 | } 308 | 309 | /** 310 | * Retrieve schema of SQL table 311 | * @return array|FALSE 312 | * @param $table string 313 | * @param $fields array|string 314 | * @param $ttl int|array 315 | **/ 316 | function schema($table,$fields=NULL,$ttl=0) { 317 | $fw=\Base::instance(); 318 | $cache=\Cache::instance(); 319 | if ($fw->CACHE && $ttl && 320 | ($cached=$cache->exists( 321 | $hash=$fw->hash($this->dsn.$table).'.schema',$result)) && 322 | $cached[0]+$ttl>microtime(TRUE)) 323 | return $result; 324 | if (strpos($table,'.')) 325 | list($schema,$table)=explode('.',$table); 326 | // Supported engines 327 | $cmd=[ 328 | 'sqlite2?'=>[ 329 | 'PRAGMA table_info(`'.$table.'`)', 330 | 'name','type','dflt_value','notnull',0,'pk',TRUE], 331 | 'mysql'=>[ 332 | 'SHOW columns FROM `'.$this->dbname.'`.`'.$table.'`', 333 | 'Field','Type','Default','Null','YES','Key','PRI'], 334 | 'mssql|sqlsrv|sybase|dblib|pgsql|odbc'=>[ 335 | 'SELECT '. 336 | 'C.COLUMN_NAME AS field,'. 337 | 'C.DATA_TYPE AS type,'. 338 | 'C.COLUMN_DEFAULT AS defval,'. 339 | 'C.IS_NULLABLE AS nullable,'. 340 | 'T.CONSTRAINT_TYPE AS pkey '. 341 | 'FROM INFORMATION_SCHEMA.COLUMNS AS C '. 342 | 'LEFT OUTER JOIN '. 343 | 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS K '. 344 | 'ON '. 345 | 'C.TABLE_NAME=K.TABLE_NAME AND '. 346 | 'C.COLUMN_NAME=K.COLUMN_NAME AND '. 347 | 'C.TABLE_SCHEMA=K.TABLE_SCHEMA '. 348 | ($this->dbname? 349 | ('AND C.TABLE_CATALOG=K.TABLE_CATALOG '):''). 350 | 'LEFT OUTER JOIN '. 351 | 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS T ON '. 352 | 'K.TABLE_NAME=T.TABLE_NAME AND '. 353 | 'K.CONSTRAINT_NAME=T.CONSTRAINT_NAME AND '. 354 | 'K.TABLE_SCHEMA=T.TABLE_SCHEMA '. 355 | ($this->dbname? 356 | ('AND K.TABLE_CATALOG=T.TABLE_CATALOG '):''). 357 | 'WHERE '. 358 | 'C.TABLE_NAME='.$this->quote($table). 359 | ($this->dbname? 360 | (' AND C.TABLE_CATALOG='. 361 | $this->quote($this->dbname)):''), 362 | 'field','type','defval','nullable','YES','pkey','PRIMARY KEY'], 363 | 'oci'=>[ 364 | 'SELECT c.column_name AS field, '. 365 | 'c.data_type AS type, '. 366 | 'c.data_default AS defval, '. 367 | 'c.nullable AS nullable, '. 368 | '(SELECT t.constraint_type '. 369 | 'FROM all_cons_columns acc '. 370 | 'LEFT OUTER JOIN all_constraints t '. 371 | 'ON acc.constraint_name=t.constraint_name '. 372 | 'WHERE acc.table_name='.$this->quote($table).' '. 373 | 'AND acc.column_name=c.column_name '. 374 | 'AND constraint_type='.$this->quote('P').') AS pkey '. 375 | 'FROM all_tab_cols c '. 376 | 'WHERE c.table_name='.$this->quote($table), 377 | 'FIELD','TYPE','DEFVAL','NULLABLE','Y','PKEY','P'] 378 | ]; 379 | if (is_string($fields)) 380 | $fields=\Base::instance()->split($fields); 381 | $conv=[ 382 | 'int\b|integer'=>\PDO::PARAM_INT, 383 | 'bool'=>\PDO::PARAM_BOOL, 384 | 'blob|bytea|image|binary'=>\PDO::PARAM_LOB, 385 | 'float|real|double|decimal|numeric'=>self::PARAM_FLOAT, 386 | '.+'=>\PDO::PARAM_STR 387 | ]; 388 | foreach ($cmd as $key=>$val) 389 | if (preg_match('/'.$key.'/',$this->engine)) { 390 | $rows=[]; 391 | foreach ($this->exec($val[0],NULL) as $row) 392 | if (!$fields || in_array($row[$val[1]],$fields)) { 393 | foreach ($conv as $regex=>$type) 394 | if (preg_match('/'.$regex.'/i',$row[$val[2]])) 395 | break; 396 | $rows[$row[$val[1]]]=[ 397 | 'type'=>$row[$val[2]], 398 | 'pdo_type'=>$type, 399 | 'default'=>is_string($row[$val[3]])? 400 | preg_replace('/^\s*([\'"])(.*)\1\s*/','\2', 401 | $row[$val[3]]):$row[$val[3]], 402 | 'nullable'=>$row[$val[4]]==$val[5], 403 | 'pkey'=>$row[$val[6]]==$val[7] 404 | ]; 405 | } 406 | if ($fw->CACHE && $ttl) 407 | // Save to cache backend 408 | $cache->set($hash,$rows,$ttl); 409 | return $rows; 410 | } 411 | user_error(sprintf(self::E_PKey,$table),E_USER_ERROR); 412 | return FALSE; 413 | } 414 | 415 | /** 416 | * Quote string 417 | * @return string 418 | * @param $val mixed 419 | * @param $type int 420 | **/ 421 | function quote($val,$type=\PDO::PARAM_STR) { 422 | return $this->engine=='odbc'? 423 | (is_string($val)? 424 | \Base::instance()->stringify(str_replace('\'','\'\'',$val)): 425 | $val): 426 | $this->pdo->quote($val,$type); 427 | } 428 | 429 | /** 430 | * Return UUID 431 | * @return string 432 | **/ 433 | function uuid() { 434 | return $this->uuid; 435 | } 436 | 437 | /** 438 | * Return parent object 439 | * @return \PDO 440 | **/ 441 | function pdo() { 442 | return $this->pdo; 443 | } 444 | 445 | /** 446 | * Return database engine 447 | * @return string 448 | **/ 449 | function driver() { 450 | return $this->engine; 451 | } 452 | 453 | /** 454 | * Return server version 455 | * @return string 456 | **/ 457 | function version() { 458 | return $this->pdo->getattribute(\PDO::ATTR_SERVER_VERSION); 459 | } 460 | 461 | /** 462 | * Return database name 463 | * @return string 464 | **/ 465 | function name() { 466 | return $this->dbname; 467 | } 468 | 469 | /** 470 | * Return quoted identifier name 471 | * @return string 472 | * @param $key 473 | * @param bool $split 474 | **/ 475 | function quotekey($key, $split=TRUE) { 476 | $delims=[ 477 | 'sqlite2?|mysql'=>'``', 478 | 'pgsql|oci'=>'""', 479 | 'mssql|sqlsrv|odbc|sybase|dblib'=>'[]' 480 | ]; 481 | $use=''; 482 | foreach ($delims as $engine=>$delim) 483 | if (preg_match('/'.$engine.'/',$this->engine)) { 484 | $use=$delim; 485 | break; 486 | } 487 | return $use[0].($split ? implode($use[1].'.'.$use[0],explode('.',$key)) 488 | : $key).$use[1]; 489 | } 490 | 491 | /** 492 | * Redirect call to PDO object 493 | * @return mixed 494 | * @param $func string 495 | * @param $args array 496 | **/ 497 | function __call($func,array $args) { 498 | return call_user_func_array([$this->pdo,$func],$args); 499 | } 500 | 501 | //! Prohibit cloning 502 | private function __clone() { 503 | } 504 | 505 | /** 506 | * Instantiate class 507 | * @param $dsn string 508 | * @param $user string 509 | * @param $pw string 510 | * @param $options array 511 | **/ 512 | function __construct($dsn,$user=NULL,$pw=NULL,array $options=NULL) { 513 | $fw=\Base::instance(); 514 | $this->uuid=$fw->hash($this->dsn=$dsn); 515 | if (preg_match('/^.+?(?:dbname|database)=(.+?)(?=;|$)/is',$dsn,$parts)) 516 | $this->dbname=$parts[1]; 517 | if (!$options) 518 | $options=[]; 519 | if (isset($parts[0]) && strstr($parts[0],':',TRUE)=='mysql') 520 | $options+=[\PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES '. 521 | strtolower(str_replace('-','',$fw->ENCODING)).';']; 522 | $this->pdo=new \PDO($dsn,$user,$pw,$options); 523 | $this->engine=$this->pdo->getattribute(\PDO::ATTR_DRIVER_NAME); 524 | } 525 | 526 | } 527 | -------------------------------------------------------------------------------- /sample-application/lib/markdown.php: -------------------------------------------------------------------------------- 1 | . 20 | 21 | */ 22 | 23 | //! Markdown-to-HTML converter 24 | class Markdown extends Prefab { 25 | 26 | protected 27 | //! Parsing rules 28 | $blocks, 29 | //! Special characters 30 | $special; 31 | 32 | /** 33 | * Process blockquote 34 | * @return string 35 | * @param $str string 36 | **/ 37 | protected function _blockquote($str) { 38 | $str=preg_replace('/(?<=^|\n)\h?>\h?(.*?(?:\n+|$))/','\1',$str); 39 | return strlen($str)? 40 | ('
'.$this->build($str).'
'."\n\n"):''; 41 | } 42 | 43 | /** 44 | * Process whitespace-prefixed code block 45 | * @return string 46 | * @param $str string 47 | **/ 48 | protected function _pre($str) { 49 | $str=preg_replace('/(?<=^|\n)(?: {4}|\t)(.+?(?:\n+|$))/','\1', 50 | $this->esc($str)); 51 | return strlen($str)? 52 | ('
'.
 53 | 				$this->esc($this->snip($str)).
 54 | 			'
'."\n\n"): 55 | ''; 56 | } 57 | 58 | /** 59 | * Process fenced code block 60 | * @return string 61 | * @param $hint string 62 | * @param $str string 63 | **/ 64 | protected function _fence($hint,$str) { 65 | $str=$this->snip($str); 66 | $fw=Base::instance(); 67 | if ($fw->HIGHLIGHT) { 68 | switch (strtolower($hint)) { 69 | case 'php': 70 | $str=$fw->highlight($str); 71 | break; 72 | case 'apache': 73 | preg_match_all('/(?<=^|\n)(\h*)'. 74 | '(?:(<\/?)(\w+)((?:\h+[^>]+)*)(>)|'. 75 | '(?:(\w+)(\h.+?)))(\h*(?:\n+|$))/', 76 | $str,$matches,PREG_SET_ORDER); 77 | $out=''; 78 | foreach ($matches as $match) 79 | $out.=$match[1]. 80 | ($match[3]? 81 | (''. 82 | $this->esc($match[2]).$match[3]. 83 | ''. 84 | ($match[4]? 85 | (''. 86 | $this->esc($match[4]). 87 | ''): 88 | ''). 89 | ''. 90 | $this->esc($match[5]). 91 | ''): 92 | (''. 93 | $match[6]. 94 | ''. 95 | ''. 96 | $this->esc($match[7]). 97 | '')). 98 | $match[8]; 99 | $str=''.$out.''; 100 | break; 101 | case 'html': 102 | preg_match_all( 103 | '/(?:(?:<(\/?)(\w+)'. 104 | '((?:\h+(?:\w+\h*=\h*)?".+?"|[^>]+)*|'. 105 | '\h+.+?)(\h*\/?)>)|(.+?))/s', 106 | $str,$matches,PREG_SET_ORDER 107 | ); 108 | $out=''; 109 | foreach ($matches as $match) { 110 | if ($match[2]) { 111 | $out.='<'. 112 | $match[1].$match[2].''; 113 | if ($match[3]) { 114 | preg_match_all( 115 | '/(?:\h+(?:(?:(\w+)\h*=\h*)?'. 116 | '(".+?")|(.+)))/', 117 | $match[3],$parts,PREG_SET_ORDER 118 | ); 119 | foreach ($parts as $part) 120 | $out.=' '. 121 | (empty($part[3])? 122 | ((empty($part[1])? 123 | '': 124 | (''. 125 | $part[1].'=')). 126 | ''. 127 | $part[2].''): 128 | (''. 129 | $part[3].'')); 130 | } 131 | $out.=''. 132 | $match[4].'>'; 133 | } 134 | else 135 | $out.=$this->esc($match[5]); 136 | } 137 | $str=''.$out.''; 138 | break; 139 | case 'ini': 140 | preg_match_all( 141 | '/(?<=^|\n)(?:'. 142 | '(;[^\n]*)|(?:<\?php.+?\?>?)|'. 143 | '(?:\[(.+?)\])|'. 144 | '(.+?)\h*=\h*'. 145 | '((?:\\\\\h*\r?\n|.+?)*)'. 146 | ')((?:\r?\n)+|$)/', 147 | $str,$matches,PREG_SET_ORDER 148 | ); 149 | $out=''; 150 | foreach ($matches as $match) { 151 | if ($match[1]) 152 | $out.=''.$match[1]. 153 | ''; 154 | elseif ($match[2]) 155 | $out.='['.$match[2].']'. 156 | ''; 157 | elseif ($match[3]) 158 | $out.=''.$match[3]. 159 | '='. 160 | ($match[4]? 161 | (''. 162 | $match[4].''):''); 163 | else 164 | $out.=$match[0]; 165 | if (isset($match[5])) 166 | $out.=$match[5]; 167 | } 168 | $str=''.$out.''; 169 | break; 170 | default: 171 | $str=''.$this->esc($str).''; 172 | break; 173 | } 174 | } 175 | else 176 | $str=''.$this->esc($str).''; 177 | return '
'.$str.'
'."\n\n"; 178 | } 179 | 180 | /** 181 | * Process horizontal rule 182 | * @return string 183 | **/ 184 | protected function _hr() { 185 | return '
'."\n\n"; 186 | } 187 | 188 | /** 189 | * Process atx-style heading 190 | * @return string 191 | * @param $type string 192 | * @param $str string 193 | **/ 194 | protected function _atx($type,$str) { 195 | $level=strlen($type); 196 | return ''. 197 | $this->scan($str).''."\n\n"; 198 | } 199 | 200 | /** 201 | * Process setext-style heading 202 | * @return string 203 | * @param $str string 204 | * @param $type string 205 | **/ 206 | protected function _setext($str,$type) { 207 | $level=strpos('=-',$type)+1; 208 | return ''. 209 | $this->scan($str).''."\n\n"; 210 | } 211 | 212 | /** 213 | * Process ordered/unordered list 214 | * @return string 215 | * @param $str string 216 | **/ 217 | protected function _li($str) { 218 | // Initialize list parser 219 | $len=strlen($str); 220 | $ptr=0; 221 | $dst=''; 222 | $first=TRUE; 223 | $tight=TRUE; 224 | $type='ul'; 225 | // Main loop 226 | while ($ptr<$len) { 227 | if (preg_match('/^\h*[*-](?:\h?[*-]){2,}(?:\n+|$)/', 228 | substr($str,$ptr),$match)) { 229 | $ptr+=strlen($match[0]); 230 | // Embedded horizontal rule 231 | return (strlen($dst)? 232 | ('<'.$type.'>'."\n".$dst.''."\n\n"):''). 233 | '
'."\n\n".$this->build(substr($str,$ptr)); 234 | } 235 | elseif (preg_match('/(?<=^|\n)([*+-]|\d+\.)\h'. 236 | '(.+?(?:\n+|$))((?:(?: {4}|\t)+.+?(?:\n+|$))*)/s', 237 | substr($str,$ptr),$match)) { 238 | $match[3]=preg_replace('/(?<=^|\n)(?: {4}|\t)/','',$match[3]); 239 | $found=FALSE; 240 | foreach (array_slice($this->blocks,0,-1) as $regex) 241 | if (preg_match($regex,$match[3])) { 242 | $found=TRUE; 243 | break; 244 | } 245 | // List 246 | if ($first) { 247 | // First pass 248 | if (is_numeric($match[1])) 249 | $type='ol'; 250 | if (preg_match('/\n{2,}$/',$match[2]. 251 | ($found?'':$match[3]))) 252 | // Loose structure; Use paragraphs 253 | $tight=FALSE; 254 | $first=FALSE; 255 | } 256 | // Strip leading whitespaces 257 | $ptr+=strlen($match[0]); 258 | $tmp=$this->snip($match[2].$match[3]); 259 | if ($tight) { 260 | if ($found) 261 | $tmp=$match[2].$this->build($this->snip($match[3])); 262 | } 263 | else 264 | $tmp=$this->build($tmp); 265 | $dst.='
  • '.$this->scan(trim($tmp)).'
  • '."\n"; 266 | } 267 | } 268 | return strlen($dst)? 269 | ('<'.$type.'>'."\n".$dst.''."\n\n"):''; 270 | } 271 | 272 | /** 273 | * Ignore raw HTML 274 | * @return string 275 | * @param $str string 276 | **/ 277 | protected function _raw($str) { 278 | return $str; 279 | } 280 | 281 | /** 282 | * Process paragraph 283 | * @return string 284 | * @param $str string 285 | **/ 286 | protected function _p($str) { 287 | $str=trim($str); 288 | if (strlen($str)) { 289 | if (preg_match('/^(.+?\n)([>#].+)$/s',$str,$parts)) 290 | return $this->_p($parts[1]).$this->build($parts[2]); 291 | $str=preg_replace_callback( 292 | '/([^<>\[]+)?(<[\?%].+?[\?%]>|<.+?>|\[.+?\]\s*\(.+?\))|'. 293 | '(.+)/s', 294 | function($expr) { 295 | $tmp=''; 296 | if (isset($expr[4])) 297 | $tmp.=$this->esc($expr[4]); 298 | else { 299 | if (isset($expr[1])) 300 | $tmp.=$this->esc($expr[1]); 301 | $tmp.=$expr[2]; 302 | if (isset($expr[3])) 303 | $tmp.=$this->esc($expr[3]); 304 | } 305 | return $tmp; 306 | }, 307 | $str 308 | ); 309 | return '

    '.$this->scan($str).'

    '."\n\n"; 310 | } 311 | return ''; 312 | } 313 | 314 | /** 315 | * Process strong/em/strikethrough spans 316 | * @return string 317 | * @param $str string 318 | **/ 319 | protected function _text($str) { 320 | $tmp=''; 321 | while ($str!=$tmp) 322 | $str=preg_replace_callback( 323 | '/(?<=\s|^)(?'.$expr[2].''; 328 | case 2: 329 | return ''.$expr[2].''; 330 | case 3: 331 | return ''.$expr[2].''; 332 | } 333 | }, 334 | preg_replace( 335 | '/(?\1', 337 | $tmp=$str 338 | ) 339 | ); 340 | return $str; 341 | } 342 | 343 | /** 344 | * Process image span 345 | * @return string 346 | * @param $str string 347 | **/ 348 | protected function _img($str) { 349 | return preg_replace_callback( 350 | '/!(?:\[(.+?)\])?\h*\(?(?:\h*"(.*?)"\h*)?\)/', 351 | function($expr) { 352 | return ''.$this->esc($expr[1]).''; 359 | }, 360 | $str 361 | ); 362 | } 363 | 364 | /** 365 | * Process anchor span 366 | * @return string 367 | * @param $str string 368 | **/ 369 | protected function _a($str) { 370 | return preg_replace_callback( 371 | '/(??(?:\h*"(.*?)"\h*)?\)/', 372 | function($expr) { 373 | return ''.$this->scan($expr[1]).''; 378 | }, 379 | $str 380 | ); 381 | } 382 | 383 | /** 384 | * Auto-convert links 385 | * @return string 386 | * @param $str string 387 | **/ 388 | protected function _auto($str) { 389 | return preg_replace_callback( 390 | '/`.*?<(.+?)>.*?`|<(.+?)>/', 391 | function($expr) { 392 | if (empty($expr[1]) && parse_url($expr[2],PHP_URL_SCHEME)) { 393 | $expr[2]=$this->esc($expr[2]); 394 | return ''.$expr[2].''; 395 | } 396 | return $expr[0]; 397 | }, 398 | $str 399 | ); 400 | } 401 | 402 | /** 403 | * Process code span 404 | * @return string 405 | * @param $str string 406 | **/ 407 | protected function _code($str) { 408 | return preg_replace_callback( 409 | '/`` (.+?) ``|(?'. 412 | $this->esc(empty($expr[1])?$expr[2]:$expr[1]).''; 413 | }, 414 | $str 415 | ); 416 | } 417 | 418 | /** 419 | * Convert characters to HTML entities 420 | * @return string 421 | * @param $str string 422 | **/ 423 | function esc($str) { 424 | if (!$this->special) 425 | $this->special=[ 426 | '...'=>'…', 427 | '(tm)'=>'™', 428 | '(r)'=>'®', 429 | '(c)'=>'©' 430 | ]; 431 | foreach ($this->special as $key=>$val) 432 | $str=preg_replace('/'.preg_quote($key,'/').'/i',$val,$str); 433 | return htmlspecialchars($str,ENT_COMPAT, 434 | Base::instance()->ENCODING,FALSE); 435 | } 436 | 437 | /** 438 | * Reduce multiple line feeds 439 | * @return string 440 | * @param $str string 441 | **/ 442 | protected function snip($str) { 443 | return preg_replace('/(?:(?<=\n)\n+)|\n+$/',"\n",$str); 444 | } 445 | 446 | /** 447 | * Scan line for convertible spans 448 | * @return string 449 | * @param $str string 450 | **/ 451 | function scan($str) { 452 | $inline=['img','a','text','auto','code']; 453 | foreach ($inline as $func) 454 | $str=$this->{'_'.$func}($str); 455 | return $str; 456 | } 457 | 458 | /** 459 | * Assemble blocks 460 | * @return string 461 | * @param $str string 462 | **/ 463 | protected function build($str) { 464 | if (!$this->blocks) { 465 | // Regexes for capturing entire blocks 466 | $this->blocks=[ 467 | 'blockquote'=>'/^(?:\h?>\h?.*?(?:\n+|$))+/', 468 | 'pre'=>'/^(?:(?: {4}|\t).+?(?:\n+|$))+/', 469 | 'fence'=>'/^`{3}\h*(\w+)?.*?[^\n]*\n+(.+?)`{3}[^\n]*'. 470 | '(?:\n+|$)/s', 471 | 'hr'=>'/^\h*[*_-](?:\h?[\*_-]){2,}\h*(?:\n+|$)/', 472 | 'atx'=>'/^\h*(#{1,6})\h?(.+?)\h*(?:#.*)?(?:\n+|$)/', 473 | 'setext'=>'/^\h*(.+?)\h*\n([=-])+\h*(?:\n+|$)/', 474 | 'li'=>'/^(?:(?:[*+-]|\d+\.)\h.+?(?:\n+|$)'. 475 | '(?:(?: {4}|\t)+.+?(?:\n+|$))*)+/s', 476 | 'raw'=>'/^((?:|'. 477 | '<(address|article|aside|audio|blockquote|canvas|dd|'. 478 | 'div|dl|fieldset|figcaption|figure|footer|form|h\d|'. 479 | 'header|hgroup|hr|noscript|object|ol|output|p|pre|'. 480 | 'section|table|tfoot|ul|video).*?'. 481 | '(?:\/>|>(?:(?>[^><]+)|(?R))*<\/\2>))'. 482 | '\h*(?:\n{2,}|\n*$)|<[\?%].+?[\?%]>\h*(?:\n?$|\n*))/s', 483 | 'p'=>'/^(.+?(?:\n{2,}|\n*$))/s' 484 | ]; 485 | } 486 | // Treat lines with nothing but whitespaces as empty lines 487 | $str=preg_replace('/\n\h+(?=\n)/',"\n",$str); 488 | // Initialize block parser 489 | $len=strlen($str); 490 | $ptr=0; 491 | $dst=''; 492 | // Main loop 493 | while ($ptr<$len) { 494 | if (preg_match('/^ {0,3}\[([^\[\]]+)\]:\s*?\s*'. 495 | '(?:"([^\n]*)")?(?:\n+|$)/s',substr($str,$ptr),$match)) { 496 | // Reference-style link; Backtrack 497 | $ptr+=strlen($match[0]); 498 | $tmp=''; 499 | // Catch line breaks in title attribute 500 | $ref=preg_replace('/\h/','\s',preg_quote($match[1],'/')); 501 | while ($dst!=$tmp) { 502 | $dst=preg_replace_callback( 503 | '/(?esc($match[2]).'"'. 510 | (empty($match[3])? 511 | '': 512 | (' title="'. 513 | $this->esc($match[3]).'"')).'>'. 514 | // Link 515 | $this->scan( 516 | empty($expr[3])? 517 | (empty($expr[1])? 518 | $expr[4]: 519 | $expr[1]): 520 | $expr[3] 521 | ).''): 522 | // Image 523 | (''.
527 | 										$this->esc($expr[3]).''); 533 | }, 534 | $tmp=$dst 535 | ); 536 | } 537 | } 538 | else 539 | foreach ($this->blocks as $func=>$regex) 540 | if (preg_match($regex,substr($str,$ptr),$match)) { 541 | $ptr+=strlen($match[0]); 542 | $dst.=call_user_func_array( 543 | [$this,'_'.$func], 544 | count($match)>1?array_slice($match,1):$match 545 | ); 546 | break; 547 | } 548 | } 549 | return $dst; 550 | } 551 | 552 | /** 553 | * Render HTML equivalent of markdown 554 | * @return string 555 | * @param $txt string 556 | **/ 557 | function convert($txt) { 558 | $txt=preg_replace_callback( 559 | '/(.+?<\/code>|'. 560 | '<[^>\n]+>|\([^\n\)]+\)|"[^"\n]+")|'. 561 | '\\\\(.)/s', 562 | function($expr) { 563 | // Process escaped characters 564 | return empty($expr[1])?$expr[2]:$expr[1]; 565 | }, 566 | $this->build(preg_replace('/\r\n|\r/',"\n",$txt)) 567 | ); 568 | return $this->snip($txt); 569 | } 570 | 571 | } 572 | --------------------------------------------------------------------------------