├── sample-application ├── ui │ ├── pesapal.html │ ├── images │ │ └── pesapal.png │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── footer.html │ ├── thanks.html │ ├── header.html │ ├── checkout.html │ └── choose.html ├── 1-choose-items-to-buy.PNG ├── 3-confirm-transaction.PNG ├── 4-buying-successful.PNG ├── 2-add-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 │ ├── PesaPal.php │ ├── template.php │ ├── smtp.php │ ├── cli │ │ └── ws.php │ └── markdown.php ├── README.md └── index.php ├── README.md └── lib └── PesaPal.php /sample-application/ui/pesapal.html: -------------------------------------------------------------------------------- 1 | 2 | {{@content | raw}} 3 | -------------------------------------------------------------------------------- /sample-application/ui/images/pesapal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-Pesapal/master/sample-application/ui/images/pesapal.png -------------------------------------------------------------------------------- /sample-application/1-choose-items-to-buy.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-Pesapal/master/sample-application/1-choose-items-to-buy.PNG -------------------------------------------------------------------------------- /sample-application/3-confirm-transaction.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-Pesapal/master/sample-application/3-confirm-transaction.PNG -------------------------------------------------------------------------------- /sample-application/4-buying-successful.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-Pesapal/master/sample-application/4-buying-successful.PNG -------------------------------------------------------------------------------- /sample-application/2-add-buyer-information.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-Pesapal/master/sample-application/2-add-buyer-information.PNG -------------------------------------------------------------------------------- /sample-application/ui/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-Pesapal/master/sample-application/ui/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /sample-application/ui/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-Pesapal/master/sample-application/ui/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /sample-application/ui/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-Pesapal/master/sample-application/ui/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /sample-application/ui/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alienwithin/F3-Pesapal/master/sample-application/ui/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /sample-application/config.ini: -------------------------------------------------------------------------------- 1 | [globals] 2 | DEBUG=3 3 | UI=ui/ 4 | [PESAPAL] 5 | consumer_key= 6 | consumer_secret= 7 | call_back= 8 | currency=KES 9 | type=MERCHANT 10 | endpoint=sandbox 11 | log=1 -------------------------------------------------------------------------------- /sample-application/ui/footer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 | -------------------------------------------------------------------------------- /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/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | F3-Pesapal Demo 12 | 13 | 14 | -------------------------------------------------------------------------------- /sample-application/ui/checkout.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Pesapal Buyer Information

5 |
6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /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/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/README.md: -------------------------------------------------------------------------------- 1 | # F3-Pesapal Sample Implementation 2 | 3 | To use this sample application update the config.ini parameters with your environment parameters i.e. 4 | 5 | ```ini 6 | [PESAPAL] 7 | consumer_key=yourConsumerKey 8 | consumer_secret=yourConsumerSecret 9 | call_back=yourCallBackURL 10 | currency=KES 11 | type=MERCHANT 12 | endpoint=sandbox 13 | log=1 14 | ``` 15 | 16 | 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 pesapal line 8 of the htaccess will be: 17 | 18 | ``` 19 | RewriteBase /pesapal 20 | ``` 21 | 22 | The application takes a 4 step- process to checkout: 23 | * Load shopping cart , currently initialized using the basket function of F3; basically this means add all items needed to cart. 24 | * Fill in mandatory buyer information amount is held from the cart so no need to re-fill it here. 25 | * Post the transaction to pesapal 26 | * redirection is done to thank you page once payment is successful. 27 | 28 | ### Note: 29 | Card's cannot be tested on test at the moment of this writing; they are purely on production ; mobile money transactions are however possible to test using the sandbox. 30 | 31 | To use in production change the endpoint to production; additionally update the keys and call back URL. 32 | 33 | ## Checkout Process 34 | 1. Select and add items to cart 35 | ![Select and add Items to cart](https://github.com/alienwithin/F3-Pesapal/raw/master/sample-application/1-choose-items-to-buy.PNG "Pesapal Integration in FatFree") 36 | 37 | 2. Fill in Buyer information 38 | 39 | ![Fill in buyer information that is mandatory](https://github.com/alienwithin/F3-Pesapal/raw/master/sample-application/2-add-buyer-information.PNG "Pesapal Integration in FatFree") 40 | 41 | 3. Pay using preferred method on Pesapal 42 | 43 | ![Pay and confirm transaction on pesapal](https://github.com/alienwithin/F3-Pesapal/raw/master/sample-application/3-confirm-transaction.PNG "Pesapal Integration in FatFree") 44 | 45 | 4. Get redirected to vendor's call back page. 46 | 47 | ![Get Redirected to thank you page](https://github.com/alienwithin/F3-Pesapal/raw/master/sample-application/4-buying-successful.PNG "Pesapal Integration in FatFree") 48 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # F3-PesaPal 2 | F3-PesaPal is a Fat Free Framework plugin that helps in easy implementation of PesaPal 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 | [PESAPAL] 10 | consumer_key=yourConsumerKey 11 | consumer_secret=yourConsumerSecret 12 | call_back=yourCallBackURL 13 | currency=KES 14 | type=MERCHANT 15 | endpoint=sandbox 16 | log=1 17 | ``` 18 | 19 | - consumer_key - Your pesapal consumer key 20 | - consumer_secret - your pesapal consumer secret 21 | - call_back - The URL that pesapal redirects your buyers to after they have logged in and clicked Continue or Pay 22 | - currency - The currency in use e.g. KES or USD 23 | - type - this can be either 'MERCHANT' or 'ORDER' 24 | - endpoint - API Endpoint, values can be 'sandbox' or 'production' 25 | - log - logs all API requests & responses to pesapal.log 26 | 27 | If you prefer you can also pass an array with above values when you instantiate the classes. 28 | 29 | ```php 30 | // F3-PesaPal config 31 | $pesaPalConfig = array( 32 | 'consumer_key'=>'yourConsumerKey', 33 | 'consumer_secret'=>'yourConsumerSecret', 34 | 'call_back'=>'yourCallBackURL', 35 | 'currency'=>'KES', 36 | 'type'=>'MERCHANT', 37 | 'endpoint'=>'sandbox', 38 | 'log'=>'1' 39 | ); 40 | 41 | // Instantiate the class with config 42 | $pesapal=new PesaPal($pesaPalConfig); 43 | ``` 44 | 45 | 46 | **Manual Install** 47 | Copy the `lib/PesaPal.php` and `lib/OAuth.php`file into your `lib/` or your AUTOLOAD folder. 48 | 49 | 50 | 51 | ## Quick Start 52 | ### PesaPal Checkout 53 | The process is going to be a multistep process as below assuming a simple form based test: 54 | 1. Create PesaPal Instance 55 | 56 | ```php 57 | $pesapal=new PesaPal; 58 | ``` 59 | 2. Populate PesaPal mandatory variables to create the valid XML to post to pesapal 60 | ```php 61 | /*Define PesaPal Mandatory Variables*/ 62 | $orderID=$f3->get('POST.TransactionID'); 63 | $first_name=$f3->get('POST.first_name'); 64 | $last_name=$f3->get('POST.last_name'); 65 | $Description=$f3->get('POST.description'); 66 | $email=$f3->get('POST.email'); 67 | $telephone=$f3->get('POST.telephone'); 68 | $totalAmount = $f3->get('POST.Amount'); 69 | /*End Define PesaPal Mandatory Variables*/ 70 | //Create PesaPal XML valid format 71 | //You can do some DB operations here based on the variables as you POST the XML 72 | $post_xml=$pesapal->create_pesapal_xml($orderID,$first_name,$last_name,$Description,$email,$telephone,$totalAmount); 73 | ``` 74 | 75 | 3. Build the proper pesapal Iframe using the XML created and pass it to a view 76 | ```php 77 | //Use the XML to genreate the IFRAME 78 | $content=$pesapal->send_payment_to_pesapal($post_xml); 79 | //Update the Fatfree Hive with the new variable and assign it a value 80 | $f3->set('content',$content); 81 | //Render on page 82 | echo \Template::instance()->render('pesapal.html'); 83 | ``` 84 | Your actual View i.e. pesapal.html will be tagged a sample is as below: 85 | ```html 86 | 87 | {{@content | raw}} 88 | 89 | ``` 90 | ### PesaPal IPN Status 91 | The plugin currently supports IPN checking by merchant reference and Pesapal Transaction ID. When Pesapal creates a Notification you can receive it and process using the function below in your IPN route , sample is as below: 92 | ```php 93 | $f3->route('GET /ipn', 94 | function ($f3) { 95 | $pesapal=new Pesapal; 96 | $orderID=$f3->get('GET.pesapal_merchant_reference'); 97 | $pesapalTrackingId=$f3->get('GET.pesapal_transaction_tracking_id'); 98 | $status=$pesapal->checkStatusUsingTrackingIdandMerchantRef($orderID,$pesapalTrackingId); 99 | //perform DB operations and Update Status to the status returned e.g. COMPLETED, PENDING, INVALID, FAILED 100 | } 101 | ); 102 | ``` 103 | 104 | View the [sample application](https://github.com/alienwithin/F3-Pesapal/tree/master/sample-application) to understand implementation aspects of the gateway and test it. 105 | 106 | ## License 107 | F3-PesaPal is licensed under GPL v.3 108 | -------------------------------------------------------------------------------- /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 Pesapal; 55 | Status of the transaction here can be considered as "PLACED" 56 | */ 57 | $f3->route('GET|POST /pesapal', 58 | function ($f3) { 59 | $basket = new \Basket(); 60 | $cartitems = $basket->find(); 61 | $pesapal= new Pesapal; 62 | /*Define Pesapal Mandatory Variables*/ 63 | $orderID=generatePesapalTransactionID(); 64 | $Description= $f3->get('POST.description'); 65 | $first_name=$f3->get('POST.first_name'); 66 | $last_name=$f3->get('POST.last_name'); 67 | $email=$f3->get('POST.email'); 68 | $telephone=$f3->get('POST.telephone'); 69 | $subtotal = $pesapal->copyBasket($cartitems); 70 | /*End Define Pesapal Mandatory Variables*/ 71 | //Create Pesapal XML valid format 72 | $post_xml=$pesapal->create_pesapal_xml($orderID,$first_name,$last_name,$Description,$email,$telephone,$subtotal); 73 | //Generate Iframe and pass it to the view 74 | $content=$pesapal->send_payment_to_pesapal($post_xml); 75 | $f3->set('content',$content); 76 | //Render on page 77 | echo \Template::instance()->render('pesapal.html'); 78 | } 79 | ); 80 | /*Pesapal responds to our call back URL with the transaction_tracking_id which will allow us to change the transaction status to "PENDING"*/ 81 | $f3->route('GET|POST /thankyou', 82 | function ($f3) { 83 | $pesapalTrackingId=$f3->get('GET.pesapal_transaction_tracking_id'); 84 | //perform DB operations update to PENDING after getting a tracking ID 85 | echo \Template::instance()->render('thanks.html'); 86 | } 87 | ); 88 | /* 89 | Pesapal hits back our IPN to tell us if the transaction was successful or not and we update accordingly. i.e. 90 | - COMPLETED 91 | - INVALID 92 | - FAILED 93 | - PENDING 94 | */ 95 | $f3->route('GET /ipn', 96 | function ($f3) { 97 | $pesapal=new Pesapal; 98 | $orderID=$f3->get('GET.pesapal_merchant_reference'); 99 | $pesapalTrackingId=$f3->get('GET.pesapal_transaction_tracking_id'); 100 | $status=$pesapal->checkStatusUsingTrackingIdandMerchantRef($orderID,$pesapalTrackingId); 101 | //perform DB operations and Update Status; 102 | } 103 | ); 104 | /* 105 | Simple function used to generate alphanumeric transaction IDs 106 | */ 107 | function generatePesapalTransactionID($length = 10) { 108 | $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 109 | $charactersLength = strlen($characters); 110 | $randomString = ''; 111 | for ($i = 0; $i < $length; $i++) { 112 | $randomString .= $characters[rand(0, $charactersLength - 1)]; 113 | } 114 | return $randomString; 115 | } 116 | $f3->run(); 117 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/PesaPal.php: -------------------------------------------------------------------------------- 1 | sync('SESSION'); 35 | if ($options == null) 36 | if ($f3->exists('PESAPAL')) 37 | $options = $f3->get('PESAPAL'); 38 | else 39 | $f3->error(500, 'No configuration options set for Pesapal on Fat Free Framework'); 40 | if ($options['endpoint'] == "production") { 41 | $this->endpoint = 'https://www.pesapal.com/'; 42 | } else { 43 | $this->endpoint = 'https://demo.pesapal.com/'; 44 | } 45 | $this->pesapalSettings['key'] = $options['consumer_key']; 46 | $this->pesapalSettings['secret'] = $options['consumer_secret']; 47 | $this->pesapalSettings['callback'] = $options['call_back']; 48 | $this->pesapalSettings['currency'] = $options['currency']; 49 | $this->pesapalSettings['type'] = $options['type']; 50 | if ($options['log']) { 51 | $this->logger = new Log('pesapal.log'); 52 | } 53 | } 54 | /** 55 | * Build array of line items & calculating item total. 56 | * @param $item_name string 57 | * @param $item_quantity integer 58 | * @param $item_price string 59 | */ 60 | function setLineItem($item_name, $item_quantity = 1, $item_price) 61 | { 62 | $i = $this->item_counter++; 63 | $this->line_items["L_PAYMENTREQUEST_0_NAME$i"] = $item_name; 64 | $this->line_items["L_PAYMENTREQUEST_0_QTY$i"] = $item_quantity; 65 | $this->line_items["L_PAYMENTREQUEST_0_AMOUNT$i"] = $item_price; 66 | $this->item_total += ($item_quantity * $item_price); 67 | } 68 | /** 69 | * Create XML to be used in Pesapal for ordering and populate mandatory fields for pesapal. 70 | * @param $orderID string 71 | * @param $first_name string 72 | * @param $last_name string 73 | * @param $Description string 74 | * @param $email string 75 | * @param $telephone string 76 | * @param $totalAmount integer 77 | */ 78 | function create_pesapal_xml($orderID,$first_name,$last_name,$Description,$email,$telephone,$totalAmount){ 79 | $f3 = Base::instance(); 80 | $getCurrency=$this->pesapalSettings['currency']; 81 | $getTransType=$this->pesapalSettings['type']; 82 | $Orderxml = " 83 | "; 94 | //protect against XSS 95 | return htmlentities($Orderxml); 96 | 97 | } 98 | /** 99 | * Generate Pesapal Iframe based on Pesapal Settings as well as XML generated 100 | * @param $post_xml string 101 | */ 102 | function send_payment_to_pesapal($post_xml){ 103 | $this->token = $this->params = NULL; 104 | /*Pick Settings from config and constructor*/ 105 | $getCallback=$this->pesapalSettings['callback']; 106 | $getEndpoint=$this->endpoint; 107 | $getConsumerKey=$this->pesapalSettings['key']; 108 | $getConsumerSecret=$this->pesapalSettings['secret']; 109 | /*End Pick Settings from config and constructor*/ 110 | $signature_method = new OAuthSignatureMethod_HMAC_SHA1(); 111 | //Build Pesapal Link 112 | $consumer = new OAuthConsumer($getConsumerKey,$getConsumerSecret); 113 | $iframe_link = $getEndpoint."api/PostPesapalDirectOrderV4"; 114 | $iframe_src = OAuthRequest::from_consumer_and_token($consumer, $token, "GET", $iframe_link, $params); 115 | $iframe_src->set_parameter("oauth_callback",$getCallback ); 116 | $iframe_src->set_parameter("pesapal_request_data", $post_xml); 117 | $iframe_src->sign_request($signature_method, $consumer, $token); 118 | //End Build Pesapal Link 119 | //Generate the proper Iframe and pass this function to your view render 120 | $iframe = ''; 123 | return $iframe; 124 | } 125 | /** 126 | * Copy basket() to Pesapal Checkout 127 | * Transfer your basket details to the Pesapal Checkout 128 | * Returns a total value of items 129 | * @param $basket object 130 | * @param $name string 131 | * @param $amount string 132 | */ 133 | function copyBasket($basket, $name = 'name', $quantity = 'qty', $amount = 'amount') 134 | { 135 | $totalamount = 0; 136 | foreach ($basket as $lineitem) { 137 | 138 | if (empty($lineitem->{$quantity})) { 139 | $lineitem->{$quantity} = 1; 140 | } 141 | 142 | $this->setLineItem($lineitem->{$name}, $lineitem->{$quantity}, $lineitem->{$amount}); 143 | $totalamount += $lineitem->{$amount} * $lineitem->{$quantity}; 144 | } 145 | 146 | return $totalamount; 147 | } 148 | /** 149 | * Check IPN status when Pesapal sends a notification change 150 | * Returns the Status of the transaction. 151 | * @param $orderID string 152 | * @param $pesapalTrackingId string 153 | */ 154 | function checkStatusUsingTrackingIdandMerchantRef($orderID,$pesapalTrackingId){ 155 | $f3 = Base::instance(); 156 | // Parameters sent to you by PesaPal IPN 157 | $pesapalNotification=$f3->get('GET.pesapal_notification_type'); 158 | /*Pick Settings from config and constructor*/ 159 | $getCallback=$this->pesapalSettings['callback']; 160 | $getEndpoint=$this->endpoint; 161 | $getConsumerKey=$this->pesapalSettings['key']; 162 | $getConsumerSecret=$this->pesapalSettings['secret']; 163 | $statusrequestAPI=$getEndpoint."api/querypaymentstatus"; 164 | /*End Pick Settings from config and constructor*/ 165 | if($pesapalNotification=="CHANGE" && $pesapalTrackingId!=''){ 166 | $token = $params = NULL; 167 | $consumer = new OAuthConsumer($getConsumerKey,$getConsumerSecret); 168 | $signature_method = new OAuthSignatureMethod_HMAC_SHA1(); 169 | $request_status = OAuthRequest::from_consumer_and_token($getConsumerKey, $token, "GET", $statusrequestAPI, $params); 170 | $request_status->set_parameter("pesapal_transaction_tracking_id",$pesapalTrackingId); 171 | $request_status->sign_request($signature_method, $consumer, $token); 172 | $web = Web::instance(); 173 | $options = array( 174 | 'method' => 'GET', 175 | ); 176 | $check_transaction_status = $web->request($request_status,$options); 177 | $pesapal_response_data=$check_transaction_status['body']; 178 | $status_headers=$check_transaction_status['headers']; 179 | switch ($pesapal_response_data) { 180 | case "INVALID": 181 | return $status_body; 182 | break; 183 | case "PENDING": 184 | return $status_body; 185 | break; 186 | case "COMPLETED": 187 | return $status_body; 188 | break; 189 | case "FAILED": 190 | return $status_body; 191 | break; 192 | default: 193 | return "Status check could not be completed."; 194 | break; 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /sample-application/lib/PesaPal.php: -------------------------------------------------------------------------------- 1 | sync('SESSION'); 35 | if ($options == null) 36 | if ($f3->exists('PESAPAL')) 37 | $options = $f3->get('PESAPAL'); 38 | else 39 | $f3->error(500, 'No configuration options set for Pesapal on Fat Free Framework'); 40 | if ($options['endpoint'] == "production") { 41 | $this->endpoint = 'https://www.pesapal.com/'; 42 | } else { 43 | $this->endpoint = 'https://demo.pesapal.com/'; 44 | } 45 | $this->pesapalSettings['key'] = $options['consumer_key']; 46 | $this->pesapalSettings['secret'] = $options['consumer_secret']; 47 | $this->pesapalSettings['callback'] = $options['call_back']; 48 | $this->pesapalSettings['currency'] = $options['currency']; 49 | $this->pesapalSettings['type'] = $options['type']; 50 | if ($options['log']) { 51 | $this->logger = new Log('pesapal.log'); 52 | } 53 | } 54 | /** 55 | * Build array of line items & calculating item total. 56 | * @param $item_name string 57 | * @param $item_quantity integer 58 | * @param $item_price string 59 | */ 60 | function setLineItem($item_name, $item_quantity = 1, $item_price) 61 | { 62 | $i = $this->item_counter++; 63 | $this->line_items["L_PAYMENTREQUEST_0_NAME$i"] = $item_name; 64 | $this->line_items["L_PAYMENTREQUEST_0_QTY$i"] = $item_quantity; 65 | $this->line_items["L_PAYMENTREQUEST_0_AMOUNT$i"] = $item_price; 66 | $this->item_total += ($item_quantity * $item_price); 67 | } 68 | /** 69 | * Create XML to be used in Pesapal for ordering and populate mandatory fields for pesapal. 70 | * @param $orderID string 71 | * @param $first_name string 72 | * @param $last_name string 73 | * @param $Description string 74 | * @param $email string 75 | * @param $telephone string 76 | * @param $totalAmount integer 77 | */ 78 | function create_pesapal_xml($orderID,$first_name,$last_name,$Description,$email,$telephone,$totalAmount){ 79 | $f3 = Base::instance(); 80 | $getCurrency=$this->pesapalSettings['currency']; 81 | $getTransType=$this->pesapalSettings['type']; 82 | $Orderxml = " 83 | "; 94 | //protect against XSS 95 | return htmlentities($Orderxml); 96 | 97 | } 98 | /** 99 | * Generate Pesapal Iframe based on Pesapal Settings as well as XML generated 100 | * @param $post_xml string 101 | */ 102 | function send_payment_to_pesapal($post_xml){ 103 | $this->token = $this->params = NULL; 104 | /*Pick Settings from config and constructor*/ 105 | $getCallback=$this->pesapalSettings['callback']; 106 | $getEndpoint=$this->endpoint; 107 | $getConsumerKey=$this->pesapalSettings['key']; 108 | $getConsumerSecret=$this->pesapalSettings['secret']; 109 | /*End Pick Settings from config and constructor*/ 110 | $signature_method = new OAuthSignatureMethod_HMAC_SHA1(); 111 | //Build Pesapal Link 112 | $consumer = new OAuthConsumer($getConsumerKey,$getConsumerSecret); 113 | $iframe_link = $getEndpoint."api/PostPesapalDirectOrderV4"; 114 | $iframe_src = OAuthRequest::from_consumer_and_token($consumer, $token, "GET", $iframe_link, $params); 115 | $iframe_src->set_parameter("oauth_callback",$getCallback ); 116 | $iframe_src->set_parameter("pesapal_request_data", $post_xml); 117 | $iframe_src->sign_request($signature_method, $consumer, $token); 118 | //End Build Pesapal Link 119 | //Generate the proper Iframe and pass this function to your view render 120 | $iframe = ''; 123 | return $iframe; 124 | } 125 | /** 126 | * Copy basket() to Pesapal Checkout 127 | * Transfer your basket details to the Pesapal Checkout 128 | * Returns a total value of items 129 | * @param $basket object 130 | * @param $name string 131 | * @param $amount string 132 | */ 133 | function copyBasket($basket, $name = 'name', $quantity = 'qty', $amount = 'amount') 134 | { 135 | $totalamount = 0; 136 | foreach ($basket as $lineitem) { 137 | 138 | if (empty($lineitem->{$quantity})) { 139 | $lineitem->{$quantity} = 1; 140 | } 141 | 142 | $this->setLineItem($lineitem->{$name}, $lineitem->{$quantity}, $lineitem->{$amount}); 143 | $totalamount += $lineitem->{$amount} * $lineitem->{$quantity}; 144 | } 145 | 146 | return $totalamount; 147 | } 148 | /** 149 | * Check IPN status when Pesapal sends a notification change 150 | * Returns the Status of the transaction. 151 | * @param $orderID string 152 | * @param $pesapalTrackingId string 153 | */ 154 | function checkStatusUsingTrackingIdandMerchantRef($orderID,$pesapalTrackingId){ 155 | $f3 = Base::instance(); 156 | // Parameters sent to you by PesaPal IPN 157 | $pesapalNotification=$f3->get('GET.pesapal_notification_type'); 158 | /*Pick Settings from config and constructor*/ 159 | $getCallback=$this->pesapalSettings['callback']; 160 | $getEndpoint=$this->endpoint; 161 | $getConsumerKey=$this->pesapalSettings['key']; 162 | $getConsumerSecret=$this->pesapalSettings['secret']; 163 | $statusrequestAPI=$getEndpoint."api/querypaymentstatus"; 164 | /*End Pick Settings from config and constructor*/ 165 | if($pesapalNotification=="CHANGE" && $pesapalTrackingId!=''){ 166 | $token = $params = NULL; 167 | $consumer = new OAuthConsumer($getConsumerKey,$getConsumerSecret); 168 | $signature_method = new OAuthSignatureMethod_HMAC_SHA1(); 169 | $request_status = OAuthRequest::from_consumer_and_token($getConsumerKey, $token, "GET", $statusrequestAPI, $params); 170 | $request_status->set_parameter("pesapal_transaction_tracking_id",$pesapalTrackingId); 171 | $request_status->sign_request($signature_method, $consumer, $token); 172 | $web = Web::instance(); 173 | $options = array( 174 | 'method' => 'GET', 175 | ); 176 | $check_transaction_status = $web->request($request_status,$options); 177 | $pesapal_response_data=$check_transaction_status['body']; 178 | $status_headers=$check_transaction_status['headers']; 179 | switch ($pesapal_response_data) { 180 | case "INVALID": 181 | return $status_body; 182 | break; 183 | case "PENDING": 184 | return $status_body; 185 | break; 186 | case "COMPLETED": 187 | return $status_body; 188 | break; 189 | case "FAILED": 190 | return $status_body; 191 | break; 192 | default: 193 | return "Status check could not be completed."; 194 | break; 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------