base.php 89 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584
  1. <?php
  2. /*
  3. Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.
  4. This file is part of the Fat-Free Framework (http://fatfreeframework.com).
  5. This is free software: you can redistribute it and/or modify it under the
  6. terms of the GNU General Public License as published by the Free Software
  7. Foundation, either version 3 of the License, or later.
  8. Fat-Free Framework is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  11. General Public License for more details.
  12. You should have received a copy of the GNU General Public License along
  13. with Fat-Free Framework. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. //! Factory class for single-instance objects
  16. abstract class Prefab {
  17. /**
  18. * Return class instance
  19. * @return static
  20. **/
  21. static function instance() {
  22. if (!Registry::exists($class=get_called_class())) {
  23. $ref=new ReflectionClass($class);
  24. $args=func_get_args();
  25. Registry::set($class,
  26. $args?$ref->newinstanceargs($args):new $class);
  27. }
  28. return Registry::get($class);
  29. }
  30. }
  31. //! Base structure
  32. final class Base extends Prefab implements ArrayAccess {
  33. //@{ Framework details
  34. const
  35. PACKAGE='Fat-Free Framework',
  36. VERSION='3.7.2-Release';
  37. //@}
  38. //@{ HTTP status codes (RFC 2616)
  39. const
  40. HTTP_100='Continue',
  41. HTTP_101='Switching Protocols',
  42. HTTP_103='Early Hints',
  43. HTTP_200='OK',
  44. HTTP_201='Created',
  45. HTTP_202='Accepted',
  46. HTTP_203='Non-Authorative Information',
  47. HTTP_204='No Content',
  48. HTTP_205='Reset Content',
  49. HTTP_206='Partial Content',
  50. HTTP_300='Multiple Choices',
  51. HTTP_301='Moved Permanently',
  52. HTTP_302='Found',
  53. HTTP_303='See Other',
  54. HTTP_304='Not Modified',
  55. HTTP_305='Use Proxy',
  56. HTTP_307='Temporary Redirect',
  57. HTTP_308='Permanent Redirect',
  58. HTTP_400='Bad Request',
  59. HTTP_401='Unauthorized',
  60. HTTP_402='Payment Required',
  61. HTTP_403='Forbidden',
  62. HTTP_404='Not Found',
  63. HTTP_405='Method Not Allowed',
  64. HTTP_406='Not Acceptable',
  65. HTTP_407='Proxy Authentication Required',
  66. HTTP_408='Request Timeout',
  67. HTTP_409='Conflict',
  68. HTTP_410='Gone',
  69. HTTP_411='Length Required',
  70. HTTP_412='Precondition Failed',
  71. HTTP_413='Request Entity Too Large',
  72. HTTP_414='Request-URI Too Long',
  73. HTTP_415='Unsupported Media Type',
  74. HTTP_416='Requested Range Not Satisfiable',
  75. HTTP_417='Expectation Failed',
  76. HTTP_421='Misdirected Request',
  77. HTTP_422='Unprocessable Entity',
  78. HTTP_423='Locked',
  79. HTTP_429='Too Many Requests',
  80. HTTP_451='Unavailable For Legal Reasons',
  81. HTTP_500='Internal Server Error',
  82. HTTP_501='Not Implemented',
  83. HTTP_502='Bad Gateway',
  84. HTTP_503='Service Unavailable',
  85. HTTP_504='Gateway Timeout',
  86. HTTP_505='HTTP Version Not Supported',
  87. HTTP_507='Insufficient Storage',
  88. HTTP_511='Network Authentication Required';
  89. //@}
  90. const
  91. //! Mapped PHP globals
  92. GLOBALS='GET|POST|COOKIE|REQUEST|SESSION|FILES|SERVER|ENV',
  93. //! HTTP verbs
  94. VERBS='GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS',
  95. //! Default directory permissions
  96. MODE=0755,
  97. //! Syntax highlighting stylesheet
  98. CSS='code.css';
  99. //@{ Request types
  100. const
  101. REQ_SYNC=1,
  102. REQ_AJAX=2,
  103. REQ_CLI=4;
  104. //@}
  105. //@{ Error messages
  106. const
  107. E_Pattern='Invalid routing pattern: %s',
  108. E_Named='Named route does not exist: %s',
  109. E_Alias='Invalid named route alias: %s',
  110. E_Fatal='Fatal error: %s',
  111. E_Open='Unable to open %s',
  112. E_Routes='No routes specified',
  113. E_Class='Invalid class %s',
  114. E_Method='Invalid method %s',
  115. E_Hive='Invalid hive key %s';
  116. //@}
  117. private
  118. //! Globals
  119. $hive,
  120. //! Initial settings
  121. $init,
  122. //! Language lookup sequence
  123. $languages,
  124. //! Mutex locks
  125. $locks=[],
  126. //! Default fallback language
  127. $fallback='en';
  128. /**
  129. * Sync PHP global with corresponding hive key
  130. * @return array
  131. * @param $key string
  132. **/
  133. function sync($key) {
  134. return $this->hive[$key]=&$GLOBALS['_'.$key];
  135. }
  136. /**
  137. * Return the parts of specified hive key
  138. * @return array
  139. * @param $key string
  140. **/
  141. private function cut($key) {
  142. return preg_split('/\[\h*[\'"]?(.+?)[\'"]?\h*\]|(->)|\./',
  143. $key,NULL,PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
  144. }
  145. /**
  146. * Replace tokenized URL with available token values
  147. * @return string
  148. * @param $url array|string
  149. * @param $args array
  150. **/
  151. function build($url,$args=[]) {
  152. $args+=$this->hive['PARAMS'];
  153. if (is_array($url))
  154. foreach ($url as &$var) {
  155. $var=$this->build($var,$args);
  156. unset($var);
  157. }
  158. else {
  159. $i=0;
  160. $url=preg_replace_callback('/(\{)?@(\w+)(?(1)\})|(\*)/',
  161. function($match) use(&$i,$args) {
  162. if (isset($match[2]) &&
  163. array_key_exists($match[2],$args))
  164. return $args[$match[2]];
  165. if (isset($match[3]) &&
  166. array_key_exists($match[3],$args)) {
  167. if (!is_array($args[$match[3]]))
  168. return $args[$match[3]];
  169. $i++;
  170. return $args[$match[3]][$i-1];
  171. }
  172. return $match[0];
  173. },$url);
  174. }
  175. return $url;
  176. }
  177. /**
  178. * Parse string containing key-value pairs
  179. * @return array
  180. * @param $str string
  181. **/
  182. function parse($str) {
  183. preg_match_all('/(\w+|\*)\h*=\h*(?:\[(.+?)\]|(.+?))(?=,|$)/',
  184. $str,$pairs,PREG_SET_ORDER);
  185. $out=[];
  186. foreach ($pairs as $pair)
  187. if ($pair[2]) {
  188. $out[$pair[1]]=[];
  189. foreach (explode(',',$pair[2]) as $val)
  190. array_push($out[$pair[1]],$val);
  191. }
  192. else
  193. $out[$pair[1]]=trim($pair[3]);
  194. return $out;
  195. }
  196. /**
  197. * Cast string variable to PHP type or constant
  198. * @param $val
  199. * @return mixed
  200. */
  201. function cast($val) {
  202. if (preg_match('/^(?:0x[0-9a-f]+|0[0-7]+|0b[01]+)$/i',$val))
  203. return intval($val,0);
  204. if (is_numeric($val))
  205. return $val+0;
  206. $val=trim($val);
  207. if (preg_match('/^\w+$/i',$val) && defined($val))
  208. return constant($val);
  209. return $val;
  210. }
  211. /**
  212. * Convert JS-style token to PHP expression
  213. * @return string
  214. * @param $str string
  215. * @param $evaluate bool compile expressions as well or only convert variable access
  216. **/
  217. function compile($str, $evaluate=TRUE) {
  218. return (!$evaluate)
  219. ? preg_replace_callback(
  220. '/^@(\w+)((?:\..+|\[(?:(?:[^\[\]]*|(?R))*)\])*)/',
  221. function($expr) {
  222. $str='$'.$expr[1];
  223. if (isset($expr[2]))
  224. $str.=preg_replace_callback(
  225. '/\.([^.\[\]]+)|\[((?:[^\[\]\'"]*|(?R))*)\]/',
  226. function($sub) {
  227. $val=isset($sub[2]) ? $sub[2] : $sub[1];
  228. if (ctype_digit($val))
  229. $val=(int)$val;
  230. $out='['.$this->export($val).']';
  231. return $out;
  232. },
  233. $expr[2]
  234. );
  235. return $str;
  236. },
  237. $str
  238. )
  239. : preg_replace_callback(
  240. '/(?<!\w)@(\w+(?:(?:\->|::)\w+)?)'.
  241. '((?:\.\w+|\[(?:(?:[^\[\]]*|(?R))*)\]|(?:\->|::)\w+|\()*)/',
  242. function($expr) {
  243. $str='$'.$expr[1];
  244. if (isset($expr[2]))
  245. $str.=preg_replace_callback(
  246. '/\.(\w+)(\()?|\[((?:[^\[\]]*|(?R))*)\]/',
  247. function($sub) {
  248. if (empty($sub[2])) {
  249. if (ctype_digit($sub[1]))
  250. $sub[1]=(int)$sub[1];
  251. $out='['.
  252. (isset($sub[3])?
  253. $this->compile($sub[3]):
  254. $this->export($sub[1])).
  255. ']';
  256. }
  257. else
  258. $out=function_exists($sub[1])?
  259. $sub[0]:
  260. ('['.$this->export($sub[1]).']'.$sub[2]);
  261. return $out;
  262. },
  263. $expr[2]
  264. );
  265. return $str;
  266. },
  267. $str
  268. );
  269. }
  270. /**
  271. * Get hive key reference/contents; Add non-existent hive keys,
  272. * array elements, and object properties by default
  273. * @return mixed
  274. * @param $key string
  275. * @param $add bool
  276. * @param $var mixed
  277. **/
  278. function &ref($key,$add=TRUE,&$var=NULL) {
  279. $null=NULL;
  280. $parts=$this->cut($key);
  281. if ($parts[0]=='SESSION') {
  282. if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
  283. session_start();
  284. $this->sync('SESSION');
  285. }
  286. elseif (!preg_match('/^\w+$/',$parts[0]))
  287. user_error(sprintf(self::E_Hive,$this->stringify($key)),
  288. E_USER_ERROR);
  289. if (is_null($var)) {
  290. if ($add)
  291. $var=&$this->hive;
  292. else
  293. $var=$this->hive;
  294. }
  295. $obj=FALSE;
  296. foreach ($parts as $part)
  297. if ($part=='->')
  298. $obj=TRUE;
  299. elseif ($obj) {
  300. $obj=FALSE;
  301. if (!is_object($var))
  302. $var=new stdClass;
  303. if ($add || property_exists($var,$part))
  304. $var=&$var->$part;
  305. else {
  306. $var=&$null;
  307. break;
  308. }
  309. }
  310. else {
  311. if (!is_array($var))
  312. $var=[];
  313. if ($add || array_key_exists($part,$var))
  314. $var=&$var[$part];
  315. else {
  316. $var=&$null;
  317. break;
  318. }
  319. }
  320. return $var;
  321. }
  322. /**
  323. * Return TRUE if hive key is set
  324. * (or return timestamp and TTL if cached)
  325. * @return bool
  326. * @param $key string
  327. * @param $val mixed
  328. **/
  329. function exists($key,&$val=NULL) {
  330. $val=$this->ref($key,FALSE);
  331. return isset($val)?
  332. TRUE:
  333. (Cache::instance()->exists($this->hash($key).'.var',$val)?:FALSE);
  334. }
  335. /**
  336. * Return TRUE if hive key is empty and not cached
  337. * @param $key string
  338. * @param $val mixed
  339. * @return bool
  340. **/
  341. function devoid($key,&$val=NULL) {
  342. $val=$this->ref($key,FALSE);
  343. return empty($val) &&
  344. (!Cache::instance()->exists($this->hash($key).'.var',$val) ||
  345. !$val);
  346. }
  347. /**
  348. * Bind value to hive key
  349. * @return mixed
  350. * @param $key string
  351. * @param $val mixed
  352. * @param $ttl int
  353. **/
  354. function set($key,$val,$ttl=0) {
  355. $time=(int)$this->hive['TIME'];
  356. if (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) {
  357. $this->set('REQUEST'.$expr[2],$val);
  358. if ($expr[1]=='COOKIE') {
  359. $parts=$this->cut($key);
  360. $jar=$this->unserialize($this->serialize($this->hive['JAR']));
  361. unset($jar['lifetime']);
  362. if (version_compare(PHP_VERSION, '7.3.0') >= 0) {
  363. unset($jar['expire']);
  364. if (isset($_COOKIE[$parts[1]]))
  365. setcookie($parts[1],NULL,['expires'=>0]+$jar);
  366. if ($ttl)
  367. $jar['expires']=$time+$ttl;
  368. setcookie($parts[1],$val,$jar);
  369. } else {
  370. unset($jar['samesite']);
  371. if (isset($_COOKIE[$parts[1]]))
  372. call_user_func_array('setcookie',
  373. array_merge([$parts[1],NULL],['expire'=>0]+$jar));
  374. if ($ttl)
  375. $jar['expire']=$time+$ttl;
  376. call_user_func_array('setcookie',[$parts[1],$val]+$jar);
  377. }
  378. $_COOKIE[$parts[1]]=$val;
  379. return $val;
  380. }
  381. }
  382. else switch ($key) {
  383. case 'CACHE':
  384. $val=Cache::instance()->load($val);
  385. break;
  386. case 'ENCODING':
  387. ini_set('default_charset',$val);
  388. if (extension_loaded('mbstring'))
  389. mb_internal_encoding($val);
  390. break;
  391. case 'FALLBACK':
  392. $this->fallback=$val;
  393. $lang=$this->language($this->hive['LANGUAGE']);
  394. case 'LANGUAGE':
  395. if (!isset($lang))
  396. $val=$this->language($val);
  397. $lex=$this->lexicon($this->hive['LOCALES'],$ttl);
  398. case 'LOCALES':
  399. if (isset($lex) || $lex=$this->lexicon($val,$ttl))
  400. foreach ($lex as $dt=>$dd) {
  401. $ref=&$this->ref($this->hive['PREFIX'].$dt);
  402. $ref=$dd;
  403. unset($ref);
  404. }
  405. break;
  406. case 'TZ':
  407. date_default_timezone_set($val);
  408. break;
  409. }
  410. $ref=&$this->ref($key);
  411. $ref=$val;
  412. if (preg_match('/^JAR\b/',$key)) {
  413. if ($key=='JAR.lifetime')
  414. $this->set('JAR.expire',$val==0?0:
  415. (is_int($val)?$time+$val:strtotime($val)));
  416. else {
  417. if ($key=='JAR.expire')
  418. $this->hive['JAR']['lifetime']=max(0,$val-$time);
  419. $jar=$this->unserialize($this->serialize($this->hive['JAR']));
  420. unset($jar['expire']);
  421. if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
  422. if (version_compare(PHP_VERSION, '7.3.0') >= 0)
  423. session_set_cookie_params($jar);
  424. else {
  425. unset($jar['samesite']);
  426. call_user_func_array('session_set_cookie_params',$jar);
  427. }
  428. }
  429. }
  430. if ($ttl)
  431. // Persist the key-value pair
  432. Cache::instance()->set($this->hash($key).'.var',$val,$ttl);
  433. return $ref;
  434. }
  435. /**
  436. * Retrieve contents of hive key
  437. * @return mixed
  438. * @param $key string
  439. * @param $args string|array
  440. **/
  441. function get($key,$args=NULL) {
  442. if (is_string($val=$this->ref($key,FALSE)) && !is_null($args))
  443. return call_user_func_array(
  444. [$this,'format'],
  445. array_merge([$val],is_array($args)?$args:[$args])
  446. );
  447. if (is_null($val)) {
  448. // Attempt to retrieve from cache
  449. if (Cache::instance()->exists($this->hash($key).'.var',$data))
  450. return $data;
  451. }
  452. return $val;
  453. }
  454. /**
  455. * Unset hive key
  456. * @param $key string
  457. **/
  458. function clear($key) {
  459. // Normalize array literal
  460. $cache=Cache::instance();
  461. $parts=$this->cut($key);
  462. if ($key=='CACHE')
  463. // Clear cache contents
  464. $cache->reset();
  465. elseif (preg_match('/^(GET|POST|COOKIE)\b(.+)/',$key,$expr)) {
  466. $this->clear('REQUEST'.$expr[2]);
  467. if ($expr[1]=='COOKIE') {
  468. $parts=$this->cut($key);
  469. $jar=$this->hive['JAR'];
  470. unset($jar['lifetime']);
  471. $jar['expire']=0;
  472. if (version_compare(PHP_VERSION, '7.3.0') >= 0) {
  473. $jar['expires']=$jar['expire'];
  474. unset($jar['expire']);
  475. setcookie($parts[1],NULL,$jar);
  476. } else {
  477. unset($jar['samesite']);
  478. call_user_func_array('setcookie',
  479. array_merge([$parts[1],NULL],$jar));
  480. }
  481. unset($_COOKIE[$parts[1]]);
  482. }
  483. }
  484. elseif ($parts[0]=='SESSION') {
  485. if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
  486. session_start();
  487. if (empty($parts[1])) {
  488. // End session
  489. session_unset();
  490. session_destroy();
  491. $this->clear('COOKIE.'.session_name());
  492. }
  493. $this->sync('SESSION');
  494. }
  495. if (!isset($parts[1]) && array_key_exists($parts[0],$this->init))
  496. // Reset global to default value
  497. $this->hive[$parts[0]]=$this->init[$parts[0]];
  498. else {
  499. $val=preg_replace('/^(\$hive)/','$this->hive',
  500. $this->compile('@hive.'.$key, FALSE));
  501. eval('unset('.$val.');');
  502. if ($parts[0]=='SESSION') {
  503. session_commit();
  504. session_start();
  505. }
  506. if ($cache->exists($hash=$this->hash($key).'.var'))
  507. // Remove from cache
  508. $cache->clear($hash);
  509. }
  510. }
  511. /**
  512. * Return TRUE if hive variable is 'on'
  513. * @return bool
  514. * @param $key string
  515. **/
  516. function checked($key) {
  517. $ref=&$this->ref($key);
  518. return $ref=='on';
  519. }
  520. /**
  521. * Return TRUE if property has public visibility
  522. * @return bool
  523. * @param $obj object
  524. * @param $key string
  525. **/
  526. function visible($obj,$key) {
  527. if (property_exists($obj,$key)) {
  528. $ref=new ReflectionProperty(get_class($obj),$key);
  529. $out=$ref->ispublic();
  530. unset($ref);
  531. return $out;
  532. }
  533. return FALSE;
  534. }
  535. /**
  536. * Multi-variable assignment using associative array
  537. * @param $vars array
  538. * @param $prefix string
  539. * @param $ttl int
  540. **/
  541. function mset(array $vars,$prefix='',$ttl=0) {
  542. foreach ($vars as $key=>$val)
  543. $this->set($prefix.$key,$val,$ttl);
  544. }
  545. /**
  546. * Publish hive contents
  547. * @return array
  548. **/
  549. function hive() {
  550. return $this->hive;
  551. }
  552. /**
  553. * Copy contents of hive variable to another
  554. * @return mixed
  555. * @param $src string
  556. * @param $dst string
  557. **/
  558. function copy($src,$dst) {
  559. $ref=&$this->ref($dst);
  560. return $ref=$this->ref($src,FALSE);
  561. }
  562. /**
  563. * Concatenate string to hive string variable
  564. * @return string
  565. * @param $key string
  566. * @param $val string
  567. **/
  568. function concat($key,$val) {
  569. $ref=&$this->ref($key);
  570. $ref.=$val;
  571. return $ref;
  572. }
  573. /**
  574. * Swap keys and values of hive array variable
  575. * @return array
  576. * @param $key string
  577. * @public
  578. **/
  579. function flip($key) {
  580. $ref=&$this->ref($key);
  581. return $ref=array_combine(array_values($ref),array_keys($ref));
  582. }
  583. /**
  584. * Add element to the end of hive array variable
  585. * @return mixed
  586. * @param $key string
  587. * @param $val mixed
  588. **/
  589. function push($key,$val) {
  590. $ref=&$this->ref($key);
  591. $ref[]=$val;
  592. return $val;
  593. }
  594. /**
  595. * Remove last element of hive array variable
  596. * @return mixed
  597. * @param $key string
  598. **/
  599. function pop($key) {
  600. $ref=&$this->ref($key);
  601. return array_pop($ref);
  602. }
  603. /**
  604. * Add element to the beginning of hive array variable
  605. * @return mixed
  606. * @param $key string
  607. * @param $val mixed
  608. **/
  609. function unshift($key,$val) {
  610. $ref=&$this->ref($key);
  611. array_unshift($ref,$val);
  612. return $val;
  613. }
  614. /**
  615. * Remove first element of hive array variable
  616. * @return mixed
  617. * @param $key string
  618. **/
  619. function shift($key) {
  620. $ref=&$this->ref($key);
  621. return array_shift($ref);
  622. }
  623. /**
  624. * Merge array with hive array variable
  625. * @return array
  626. * @param $key string
  627. * @param $src string|array
  628. * @param $keep bool
  629. **/
  630. function merge($key,$src,$keep=FALSE) {
  631. $ref=&$this->ref($key);
  632. if (!$ref)
  633. $ref=[];
  634. $out=array_merge($ref,is_string($src)?$this->hive[$src]:$src);
  635. if ($keep)
  636. $ref=$out;
  637. return $out;
  638. }
  639. /**
  640. * Extend hive array variable with default values from $src
  641. * @return array
  642. * @param $key string
  643. * @param $src string|array
  644. * @param $keep bool
  645. **/
  646. function extend($key,$src,$keep=FALSE) {
  647. $ref=&$this->ref($key);
  648. if (!$ref)
  649. $ref=[];
  650. $out=array_replace_recursive(
  651. is_string($src)?$this->hive[$src]:$src,$ref);
  652. if ($keep)
  653. $ref=$out;
  654. return $out;
  655. }
  656. /**
  657. * Convert backslashes to slashes
  658. * @return string
  659. * @param $str string
  660. **/
  661. function fixslashes($str) {
  662. return $str?strtr($str,'\\','/'):$str;
  663. }
  664. /**
  665. * Split comma-, semi-colon, or pipe-separated string
  666. * @return array
  667. * @param $str string
  668. * @param $noempty bool
  669. **/
  670. function split($str,$noempty=TRUE) {
  671. return array_map('trim',
  672. preg_split('/[,;|]/',$str,0,$noempty?PREG_SPLIT_NO_EMPTY:0));
  673. }
  674. /**
  675. * Convert PHP expression/value to compressed exportable string
  676. * @return string
  677. * @param $arg mixed
  678. * @param $stack array
  679. **/
  680. function stringify($arg,array $stack=NULL) {
  681. if ($stack) {
  682. foreach ($stack as $node)
  683. if ($arg===$node)
  684. return '*RECURSION*';
  685. }
  686. else
  687. $stack=[];
  688. switch (gettype($arg)) {
  689. case 'object':
  690. $str='';
  691. foreach (get_object_vars($arg) as $key=>$val)
  692. $str.=($str?',':'').
  693. $this->export($key).'=>'.
  694. $this->stringify($val,
  695. array_merge($stack,[$arg]));
  696. return get_class($arg).'::__set_state(['.$str.'])';
  697. case 'array':
  698. $str='';
  699. $num=isset($arg[0]) &&
  700. ctype_digit(implode('',array_keys($arg)));
  701. foreach ($arg as $key=>$val)
  702. $str.=($str?',':'').
  703. ($num?'':($this->export($key).'=>')).
  704. $this->stringify($val,array_merge($stack,[$arg]));
  705. return '['.$str.']';
  706. default:
  707. return $this->export($arg);
  708. }
  709. }
  710. /**
  711. * Flatten array values and return as CSV string
  712. * @return string
  713. * @param $args array
  714. **/
  715. function csv(array $args) {
  716. return implode(',',array_map('stripcslashes',
  717. array_map([$this,'stringify'],$args)));
  718. }
  719. /**
  720. * Convert snakecase string to camelcase
  721. * @return string
  722. * @param $str string
  723. **/
  724. function camelcase($str) {
  725. return preg_replace_callback(
  726. '/_(\pL)/u',
  727. function($match) {
  728. return strtoupper($match[1]);
  729. },
  730. $str
  731. );
  732. }
  733. /**
  734. * Convert camelcase string to snakecase
  735. * @return string
  736. * @param $str string
  737. **/
  738. function snakecase($str) {
  739. return strtolower(preg_replace('/(?!^)\p{Lu}/u','_\0',$str));
  740. }
  741. /**
  742. * Return -1 if specified number is negative, 0 if zero,
  743. * or 1 if the number is positive
  744. * @return int
  745. * @param $num mixed
  746. **/
  747. function sign($num) {
  748. return $num?($num/abs($num)):0;
  749. }
  750. /**
  751. * Extract values of array whose keys start with the given prefix
  752. * @return array
  753. * @param $arr array
  754. * @param $prefix string
  755. **/
  756. function extract($arr,$prefix) {
  757. $out=[];
  758. foreach (preg_grep('/^'.preg_quote($prefix,'/').'/',array_keys($arr))
  759. as $key)
  760. $out[substr($key,strlen($prefix))]=$arr[$key];
  761. return $out;
  762. }
  763. /**
  764. * Convert class constants to array
  765. * @return array
  766. * @param $class object|string
  767. * @param $prefix string
  768. **/
  769. function constants($class,$prefix='') {
  770. $ref=new ReflectionClass($class);
  771. return $this->extract($ref->getconstants(),$prefix);
  772. }
  773. /**
  774. * Generate 64bit/base36 hash
  775. * @return string
  776. * @param $str
  777. **/
  778. function hash($str) {
  779. return str_pad(base_convert(
  780. substr(sha1($str),-16),16,36),11,'0',STR_PAD_LEFT);
  781. }
  782. /**
  783. * Return Base64-encoded equivalent
  784. * @return string
  785. * @param $data string
  786. * @param $mime string
  787. **/
  788. function base64($data,$mime) {
  789. return 'data:'.$mime.';base64,'.base64_encode($data);
  790. }
  791. /**
  792. * Convert special characters to HTML entities
  793. * @return string
  794. * @param $str string
  795. **/
  796. function encode($str) {
  797. return @htmlspecialchars($str,$this->hive['BITMASK'],
  798. $this->hive['ENCODING'])?:$this->scrub($str);
  799. }
  800. /**
  801. * Convert HTML entities back to characters
  802. * @return string
  803. * @param $str string
  804. **/
  805. function decode($str) {
  806. return htmlspecialchars_decode($str,$this->hive['BITMASK']);
  807. }
  808. /**
  809. * Invoke callback recursively for all data types
  810. * @return mixed
  811. * @param $arg mixed
  812. * @param $func callback
  813. * @param $stack array
  814. **/
  815. function recursive($arg,$func,$stack=[]) {
  816. if ($stack) {
  817. foreach ($stack as $node)
  818. if ($arg===$node)
  819. return $arg;
  820. }
  821. switch (gettype($arg)) {
  822. case 'object':
  823. $ref=new ReflectionClass($arg);
  824. if ($ref->iscloneable()) {
  825. $arg=clone($arg);
  826. $cast=is_a($arg,'IteratorAggregate')?
  827. iterator_to_array($arg):get_object_vars($arg);
  828. foreach ($cast as $key=>$val)
  829. $arg->$key=$this->recursive(
  830. $val,$func,array_merge($stack,[$arg]));
  831. }
  832. return $arg;
  833. case 'array':
  834. $copy=[];
  835. foreach ($arg as $key=>$val)
  836. $copy[$key]=$this->recursive($val,$func,
  837. array_merge($stack,[$arg]));
  838. return $copy;
  839. }
  840. return $func($arg);
  841. }
  842. /**
  843. * Remove HTML tags (except those enumerated) and non-printable
  844. * characters to mitigate XSS/code injection attacks
  845. * @return mixed
  846. * @param $arg mixed
  847. * @param $tags string
  848. **/
  849. function clean($arg,$tags=NULL) {
  850. return $this->recursive($arg,
  851. function($val) use($tags) {
  852. if ($tags!='*')
  853. $val=trim(strip_tags($val,
  854. '<'.implode('><',$this->split($tags)).'>'));
  855. return trim(preg_replace(
  856. '/[\x00-\x08\x0B\x0C\x0E-\x1F]/','',$val));
  857. }
  858. );
  859. }
  860. /**
  861. * Similar to clean(), except that variable is passed by reference
  862. * @return mixed
  863. * @param $var mixed
  864. * @param $tags string
  865. **/
  866. function scrub(&$var,$tags=NULL) {
  867. return $var=$this->clean($var,$tags);
  868. }
  869. /**
  870. * Return locale-aware formatted string
  871. * @return string
  872. **/
  873. function format() {
  874. $args=func_get_args();
  875. $val=array_shift($args);
  876. // Get formatting rules
  877. $conv=localeconv();
  878. return preg_replace_callback(
  879. '/\{\s*(?P<pos>\d+)\s*(?:,\s*(?P<type>\w+)\s*'.
  880. '(?:,\s*(?P<mod>(?:\w+(?:\s*\{.+?\}\s*,?\s*)?)*)'.
  881. '(?:,\s*(?P<prop>.+?))?)?)?\s*\}/',
  882. function($expr) use($args,$conv) {
  883. /**
  884. * @var string $pos
  885. * @var string $mod
  886. * @var string $type
  887. * @var string $prop
  888. */
  889. extract($expr);
  890. /**
  891. * @var string $thousands_sep
  892. * @var string $negative_sign
  893. * @var string $positive_sign
  894. * @var string $frac_digits
  895. * @var string $decimal_point
  896. * @var string $int_curr_symbol
  897. * @var string $currency_symbol
  898. */
  899. extract($conv);
  900. if (!array_key_exists($pos,$args))
  901. return $expr[0];
  902. if (isset($type)) {
  903. if (isset($this->hive['FORMATS'][$type]))
  904. return $this->call(
  905. $this->hive['FORMATS'][$type],
  906. [
  907. $args[$pos],
  908. isset($mod)?$mod:null,
  909. isset($prop)?$prop:null
  910. ]
  911. );
  912. switch ($type) {
  913. case 'plural':
  914. preg_match_all('/(?<tag>\w+)'.
  915. '(?:\s*\{\s*(?<data>.+?)\s*\})/',
  916. $mod,$matches,PREG_SET_ORDER);
  917. $ord=['zero','one','two'];
  918. foreach ($matches as $match) {
  919. /** @var string $tag */
  920. /** @var string $data */
  921. extract($match);
  922. if (isset($ord[$args[$pos]]) &&
  923. $tag==$ord[$args[$pos]] || $tag=='other')
  924. return str_replace('#',$args[$pos],$data);
  925. }
  926. case 'number':
  927. if (isset($mod))
  928. switch ($mod) {
  929. case 'integer':
  930. return number_format(
  931. $args[$pos],0,'',$thousands_sep);
  932. case 'currency':
  933. $int=$cstm=FALSE;
  934. if (isset($prop) &&
  935. $cstm=!$int=($prop=='int'))
  936. $currency_symbol=$prop;
  937. if (!$cstm &&
  938. function_exists('money_format') &&
  939. version_compare(PHP_VERSION,'7.4.0')<0)
  940. return money_format(
  941. '%'.($int?'i':'n'),$args[$pos]);
  942. $fmt=[
  943. 0=>'(nc)',1=>'(n c)',
  944. 2=>'(nc)',10=>'+nc',
  945. 11=>'+n c',12=>'+ nc',
  946. 20=>'nc+',21=>'n c+',
  947. 22=>'nc +',30=>'n+c',
  948. 31=>'n +c',32=>'n+ c',
  949. 40=>'nc+',41=>'n c+',
  950. 42=>'nc +',100=>'(cn)',
  951. 101=>'(c n)',102=>'(cn)',
  952. 110=>'+cn',111=>'+c n',
  953. 112=>'+ cn',120=>'cn+',
  954. 121=>'c n+',122=>'cn +',
  955. 130=>'+cn',131=>'+c n',
  956. 132=>'+ cn',140=>'c+n',
  957. 141=>'c+ n',142=>'c +n'
  958. ];
  959. if ($args[$pos]<0) {
  960. $sgn=$negative_sign;
  961. $pre='n';
  962. }
  963. else {
  964. $sgn=$positive_sign;
  965. $pre='p';
  966. }
  967. return str_replace(
  968. ['+','n','c'],
  969. [$sgn,number_format(
  970. abs($args[$pos]),
  971. $frac_digits,
  972. $decimal_point,
  973. $thousands_sep),
  974. $int?$int_curr_symbol
  975. :$currency_symbol],
  976. $fmt[(int)(
  977. (${$pre.'_cs_precedes'}%2).
  978. (${$pre.'_sign_posn'}%5).
  979. (${$pre.'_sep_by_space'}%3)
  980. )]
  981. );
  982. case 'percent':
  983. return number_format(
  984. $args[$pos]*100,0,$decimal_point,
  985. $thousands_sep).'%';
  986. }
  987. $frac=$args[$pos]-(int)$args[$pos];
  988. return number_format(
  989. $args[$pos],
  990. isset($prop)?
  991. $prop:
  992. ($frac?strlen($frac)-2:0),
  993. $decimal_point,$thousands_sep);
  994. case 'date':
  995. if (empty($mod) || $mod=='short')
  996. $prop='%x';
  997. elseif ($mod=='full')
  998. $prop='%A, %d %B %Y';
  999. elseif ($mod!='custom')
  1000. $prop='%d %B %Y';
  1001. return strftime($prop,$args[$pos]);
  1002. case 'time':
  1003. if (empty($mod) || $mod=='short')
  1004. $prop='%X';
  1005. elseif ($mod!='custom')
  1006. $prop='%r';
  1007. return strftime($prop,$args[$pos]);
  1008. default:
  1009. return $expr[0];
  1010. }
  1011. }
  1012. return $args[$pos];
  1013. },
  1014. $val
  1015. );
  1016. }
  1017. /**
  1018. * Return string representation of expression
  1019. * @return string
  1020. * @param $expr mixed
  1021. **/
  1022. function export($expr) {
  1023. return var_export($expr,TRUE);
  1024. }
  1025. /**
  1026. * Assign/auto-detect language
  1027. * @return string
  1028. * @param $code string
  1029. **/
  1030. function language($code) {
  1031. $code=preg_replace('/\h+|;q=[0-9.]+/','',$code);
  1032. $code.=($code?',':'').$this->fallback;
  1033. $this->languages=[];
  1034. foreach (array_reverse(explode(',',$code)) as $lang)
  1035. if (preg_match('/^(\w{2})(?:-(\w{2}))?\b/i',$lang,$parts)) {
  1036. // Generic language
  1037. array_unshift($this->languages,$parts[1]);
  1038. if (isset($parts[2])) {
  1039. // Specific language
  1040. $parts[0]=$parts[1].'-'.($parts[2]=strtoupper($parts[2]));
  1041. array_unshift($this->languages,$parts[0]);
  1042. }
  1043. }
  1044. $this->languages=array_unique($this->languages);
  1045. $locales=[];
  1046. $windows=preg_match('/^win/i',PHP_OS);
  1047. // Work around PHP's Turkish locale bug
  1048. foreach (preg_grep('/^(?!tr)/i',$this->languages) as $locale) {
  1049. if ($windows) {
  1050. $parts=explode('-',$locale);
  1051. $locale=@constant('ISO::LC_'.$parts[0]);
  1052. if (isset($parts[1]) &&
  1053. $country=@constant('ISO::CC_'.strtolower($parts[1])))
  1054. $locale.='-'.$country;
  1055. }
  1056. $locale=str_replace('-','_',$locale);
  1057. $locales[]=$locale.'.'.ini_get('default_charset');
  1058. $locales[]=$locale;
  1059. }
  1060. setlocale(LC_ALL,$locales);
  1061. return $this->hive['LANGUAGE']=implode(',',$this->languages);
  1062. }
  1063. /**
  1064. * Return lexicon entries
  1065. * @return array
  1066. * @param $path string
  1067. * @param $ttl int
  1068. **/
  1069. function lexicon($path,$ttl=0) {
  1070. $languages=$this->languages?:explode(',',$this->fallback);
  1071. $cache=Cache::instance();
  1072. if ($ttl && $cache->exists(
  1073. $hash=$this->hash(implode(',',$languages).$path).'.dic',$lex))
  1074. return $lex;
  1075. $lex=[];
  1076. foreach ($languages as $lang)
  1077. foreach ($this->split($path) as $dir)
  1078. if ((is_file($file=($base=$dir.$lang).'.php') ||
  1079. is_file($file=$base.'.php')) &&
  1080. is_array($dict=require($file)))
  1081. $lex+=$dict;
  1082. elseif (is_file($file=$base.'.json') &&
  1083. is_array($dict=json_decode(file_get_contents($file), true)))
  1084. $lex+=$dict;
  1085. elseif (is_file($file=$base.'.ini')) {
  1086. preg_match_all(
  1087. '/(?<=^|\n)(?:'.
  1088. '\[(?<prefix>.+?)\]|'.
  1089. '(?<lval>[^\h\r\n;].*?)\h*=\h*'.
  1090. '(?<rval>(?:\\\\\h*\r?\n|.+?)*)'.
  1091. ')(?=\r?\n|$)/',
  1092. $this->read($file),$matches,PREG_SET_ORDER);
  1093. if ($matches) {
  1094. $prefix='';
  1095. foreach ($matches as $match)
  1096. if ($match['prefix'])
  1097. $prefix=$match['prefix'].'.';
  1098. elseif (!array_key_exists(
  1099. $key=$prefix.$match['lval'],$lex))
  1100. $lex[$key]=trim(preg_replace(
  1101. '/\\\\\h*\r?\n/',"\n",$match['rval']));
  1102. }
  1103. }
  1104. if ($ttl)
  1105. $cache->set($hash,$lex,$ttl);
  1106. return $lex;
  1107. }
  1108. /**
  1109. * Return string representation of PHP value
  1110. * @return string
  1111. * @param $arg mixed
  1112. **/
  1113. function serialize($arg) {
  1114. switch (strtolower($this->hive['SERIALIZER'])) {
  1115. case 'igbinary':
  1116. return igbinary_serialize($arg);
  1117. default:
  1118. return serialize($arg);
  1119. }
  1120. }
  1121. /**
  1122. * Return PHP value derived from string
  1123. * @return string
  1124. * @param $arg mixed
  1125. **/
  1126. function unserialize($arg) {
  1127. switch (strtolower($this->hive['SERIALIZER'])) {
  1128. case 'igbinary':
  1129. return igbinary_unserialize($arg);
  1130. default:
  1131. return unserialize($arg);
  1132. }
  1133. }
  1134. /**
  1135. * Send HTTP status header; Return text equivalent of status code
  1136. * @return string
  1137. * @param $code int
  1138. **/
  1139. function status($code) {
  1140. $reason=@constant('self::HTTP_'.$code);
  1141. if (!$this->hive['CLI'] && !headers_sent())
  1142. header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$reason);
  1143. return $reason;
  1144. }
  1145. /**
  1146. * Send cache metadata to HTTP client
  1147. * @param $secs int
  1148. **/
  1149. function expire($secs=0) {
  1150. if (!$this->hive['CLI'] && !headers_sent()) {
  1151. $secs=(int)$secs;
  1152. if ($this->hive['PACKAGE'])
  1153. header('X-Powered-By: '.$this->hive['PACKAGE']);
  1154. if ($this->hive['XFRAME'])
  1155. header('X-Frame-Options: '.$this->hive['XFRAME']);
  1156. header('X-XSS-Protection: 1; mode=block');
  1157. header('X-Content-Type-Options: nosniff');
  1158. if ($this->hive['VERB']=='GET' && $secs) {
  1159. $time=microtime(TRUE);
  1160. header_remove('Pragma');
  1161. header('Cache-Control: max-age='.$secs);
  1162. header('Expires: '.gmdate('r',$time+$secs));
  1163. header('Last-Modified: '.gmdate('r'));
  1164. }
  1165. else {
  1166. header('Pragma: no-cache');
  1167. header('Cache-Control: no-cache, no-store, must-revalidate');
  1168. header('Expires: '.gmdate('r',0));
  1169. }
  1170. }
  1171. }
  1172. /**
  1173. * Return HTTP user agent
  1174. * @return string
  1175. **/
  1176. function agent() {
  1177. $headers=$this->hive['HEADERS'];
  1178. return isset($headers['X-Operamini-Phone-UA'])?
  1179. $headers['X-Operamini-Phone-UA']:
  1180. (isset($headers['X-Skyfire-Phone'])?
  1181. $headers['X-Skyfire-Phone']:
  1182. (isset($headers['User-Agent'])?
  1183. $headers['User-Agent']:''));
  1184. }
  1185. /**
  1186. * Return TRUE if XMLHttpRequest detected
  1187. * @return bool
  1188. **/
  1189. function ajax() {
  1190. $headers=$this->hive['HEADERS'];
  1191. return isset($headers['X-Requested-With']) &&
  1192. $headers['X-Requested-With']=='XMLHttpRequest';
  1193. }
  1194. /**
  1195. * Sniff IP address
  1196. * @return string
  1197. **/
  1198. function ip() {
  1199. $headers=$this->hive['HEADERS'];
  1200. return isset($headers['Client-IP'])?
  1201. $headers['Client-IP']:
  1202. (isset($headers['X-Forwarded-For'])?
  1203. explode(',',$headers['X-Forwarded-For'])[0]:
  1204. (isset($_SERVER['REMOTE_ADDR'])?
  1205. $_SERVER['REMOTE_ADDR']:''));
  1206. }
  1207. /**
  1208. * Return filtered stack trace as a formatted string (or array)
  1209. * @return string|array
  1210. * @param $trace array|NULL
  1211. * @param $format bool
  1212. **/
  1213. function trace(array $trace=NULL,$format=TRUE) {
  1214. if (!$trace) {
  1215. $trace=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
  1216. $frame=$trace[0];
  1217. if (isset($frame['file']) && $frame['file']==__FILE__)
  1218. array_shift($trace);
  1219. }
  1220. $debug=$this->hive['DEBUG'];
  1221. $trace=array_filter(
  1222. $trace,
  1223. function($frame) use($debug) {
  1224. return isset($frame['file']) &&
  1225. ($debug>1 ||
  1226. ($frame['file']!=__FILE__ || $debug) &&
  1227. (empty($frame['function']) ||
  1228. !preg_match('/^(?:(?:trigger|user)_error|'.
  1229. '__call|call_user_func)/',$frame['function'])));
  1230. }
  1231. );
  1232. if (!$format)
  1233. return $trace;
  1234. $out='';
  1235. $eol="\n";
  1236. // Analyze stack trace
  1237. foreach ($trace as $frame) {
  1238. $line='';
  1239. if (isset($frame['class']))
  1240. $line.=$frame['class'].$frame['type'];
  1241. if (isset($frame['function']))
  1242. $line.=$frame['function'].'('.
  1243. ($debug>2 && isset($frame['args'])?
  1244. $this->csv($frame['args']):'').')';
  1245. $src=$this->fixslashes(str_replace($_SERVER['DOCUMENT_ROOT'].
  1246. '/','',$frame['file'])).':'.$frame['line'];
  1247. $out.='['.$src.'] '.$line.$eol;
  1248. }
  1249. return $out;
  1250. }
  1251. /**
  1252. * Log error; Execute ONERROR handler if defined, else display
  1253. * default error page (HTML for synchronous requests, JSON string
  1254. * for AJAX requests)
  1255. * @param $code int
  1256. * @param $text string
  1257. * @param $trace array
  1258. * @param $level int
  1259. **/
  1260. function error($code,$text='',array $trace=NULL,$level=0) {
  1261. $prior=$this->hive['ERROR'];
  1262. $header=$this->status($code);
  1263. $req=$this->hive['VERB'].' '.$this->hive['PATH'];
  1264. if ($this->hive['QUERY'])
  1265. $req.='?'.$this->hive['QUERY'];
  1266. if (!$text)
  1267. $text='HTTP '.$code.' ('.$req.')';
  1268. $trace=$this->trace($trace);
  1269. $loggable=$this->hive['LOGGABLE'];
  1270. if (!is_array($loggable))
  1271. $loggable=$this->split($loggable);
  1272. foreach ($loggable as $status)
  1273. if ($status=='*' ||
  1274. preg_match('/^'.preg_replace('/\D/','\d',$status).'$/',$code)) {
  1275. error_log($text);
  1276. foreach (explode("\n",$trace) as $nexus)
  1277. if ($nexus)
  1278. error_log($nexus);
  1279. break;
  1280. }
  1281. if ($highlight=!$this->hive['CLI'] && !$this->hive['AJAX'] &&
  1282. $this->hive['HIGHLIGHT'] && is_file($css=__DIR__.'/'.self::CSS))
  1283. $trace=$this->highlight($trace);
  1284. $this->hive['ERROR']=[
  1285. 'status'=>$header,
  1286. 'code'=>$code,
  1287. 'text'=>$text,
  1288. 'trace'=>$trace,
  1289. 'level'=>$level
  1290. ];
  1291. $this->expire(-1);
  1292. $handler=$this->hive['ONERROR'];
  1293. $this->hive['ONERROR']=NULL;
  1294. $eol="\n";
  1295. if ((!$handler ||
  1296. $this->call($handler,[$this,$this->hive['PARAMS']],
  1297. 'beforeroute,afterroute')===FALSE) &&
  1298. !$prior && !$this->hive['CLI'] && !$this->hive['QUIET'])
  1299. echo $this->hive['AJAX']?
  1300. json_encode(
  1301. array_diff_key(
  1302. $this->hive['ERROR'],
  1303. $this->hive['DEBUG']?
  1304. []:
  1305. ['trace'=>1]
  1306. )
  1307. ):
  1308. ('<!DOCTYPE html>'.$eol.
  1309. '<html>'.$eol.
  1310. '<head>'.
  1311. '<title>'.$code.' '.$header.'</title>'.
  1312. ($highlight?
  1313. ('<style>'.$this->read($css).'</style>'):'').
  1314. '</head>'.$eol.
  1315. '<body>'.$eol.
  1316. '<h1>'.$header.'</h1>'.$eol.
  1317. '<p>'.$this->encode($text?:$req).'</p>'.$eol.
  1318. ($this->hive['DEBUG']?('<pre>'.$trace.'</pre>'.$eol):'').
  1319. '</body>'.$eol.
  1320. '</html>');
  1321. if ($this->hive['HALT'])
  1322. die(1);
  1323. }
  1324. /**
  1325. * Mock HTTP request
  1326. * @return mixed
  1327. * @param $pattern string
  1328. * @param $args array
  1329. * @param $headers array
  1330. * @param $body string
  1331. **/
  1332. function mock($pattern,
  1333. array $args=NULL,array $headers=NULL,$body=NULL) {
  1334. if (!$args)
  1335. $args=[];
  1336. $types=['sync','ajax','cli'];
  1337. preg_match('/([\|\w]+)\h+(?:@(\w+)(?:(\(.+?)\))*|([^\h]+))'.
  1338. '(?:\h+\[('.implode('|',$types).')\])?/',$pattern,$parts);
  1339. $verb=strtoupper($parts[1]);
  1340. if ($parts[2]) {
  1341. if (empty($this->hive['ALIASES'][$parts[2]]))
  1342. user_error(sprintf(self::E_Named,$parts[2]),E_USER_ERROR);
  1343. $parts[4]=$this->hive['ALIASES'][$parts[2]];
  1344. $parts[4]=$this->build($parts[4],
  1345. isset($parts[3])?$this->parse($parts[3]):[]);
  1346. }
  1347. if (empty($parts[4]))
  1348. user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR);
  1349. $url=parse_url($parts[4]);
  1350. parse_str(@$url['query'],$GLOBALS['_GET']);
  1351. if (preg_match('/GET|HEAD/',$verb))
  1352. $GLOBALS['_GET']=array_merge($GLOBALS['_GET'],$args);
  1353. $GLOBALS['_POST']=$verb=='POST'?$args:[];
  1354. $GLOBALS['_REQUEST']=array_merge($GLOBALS['_GET'],$GLOBALS['_POST']);
  1355. foreach ($headers?:[] as $key=>$val)
  1356. $_SERVER['HTTP_'.strtr(strtoupper($key),'-','_')]=$val;
  1357. $this->hive['VERB']=$verb;
  1358. $this->hive['PATH']=$url['path'];
  1359. $this->hive['URI']=$this->hive['BASE'].$url['path'];
  1360. if ($GLOBALS['_GET'])
  1361. $this->hive['URI'].='?'.http_build_query($GLOBALS['_GET']);
  1362. $this->hive['BODY']='';
  1363. if (!preg_match('/GET|HEAD/',$verb))
  1364. $this->hive['BODY']=$body?:http_build_query($args);
  1365. $this->hive['AJAX']=isset($parts[5]) &&
  1366. preg_match('/ajax/i',$parts[5]);
  1367. $this->hive['CLI']=isset($parts[5]) &&
  1368. preg_match('/cli/i',$parts[5]);
  1369. return $this->run();
  1370. }
  1371. /**
  1372. * Assemble url from alias name
  1373. * @return string
  1374. * @param $name string
  1375. * @param $params array|string
  1376. * @param $query string|array
  1377. * @param $fragment string
  1378. **/
  1379. function alias($name,$params=[],$query=NULL,$fragment=NULL) {
  1380. if (!is_array($params))
  1381. $params=$this->parse($params);
  1382. if (empty($this->hive['ALIASES'][$name]))
  1383. user_error(sprintf(self::E_Named,$name),E_USER_ERROR);
  1384. $url=$this->build($this->hive['ALIASES'][$name],$params);
  1385. if (is_array($query))
  1386. $query=http_build_query($query);
  1387. return $url.($query?('?'.$query):'').($fragment?'#'.$fragment:'');
  1388. }
  1389. /**
  1390. * Bind handler to route pattern
  1391. * @return NULL
  1392. * @param $pattern string|array
  1393. * @param $handler callback
  1394. * @param $ttl int
  1395. * @param $kbps int
  1396. **/
  1397. function route($pattern,$handler,$ttl=0,$kbps=0) {
  1398. $types=['sync','ajax','cli'];
  1399. $alias=null;
  1400. if (is_array($pattern)) {
  1401. foreach ($pattern as $item)
  1402. $this->route($item,$handler,$ttl,$kbps);
  1403. return;
  1404. }
  1405. preg_match('/([\|\w]+)\h+(?:(?:@?(.+?)\h*:\h*)?(@(\w+)|[^\h]+))'.
  1406. '(?:\h+\[('.implode('|',$types).')\])?/u',$pattern,$parts);
  1407. if (isset($parts[2]) && $parts[2]) {
  1408. if (!preg_match('/^\w+$/',$parts[2]))
  1409. user_error(sprintf(self::E_Alias,$parts[2]),E_USER_ERROR);
  1410. $this->hive['ALIASES'][$alias=$parts[2]]=$parts[3];
  1411. }
  1412. elseif (!empty($parts[4])) {
  1413. if (empty($this->hive['ALIASES'][$parts[4]]))
  1414. user_error(sprintf(self::E_Named,$parts[4]),E_USER_ERROR);
  1415. $parts[3]=$this->hive['ALIASES'][$alias=$parts[4]];
  1416. }
  1417. if (empty($parts[3]))
  1418. user_error(sprintf(self::E_Pattern,$pattern),E_USER_ERROR);
  1419. $type=empty($parts[5])?0:constant('self::REQ_'.strtoupper($parts[5]));
  1420. foreach ($this->split($parts[1]) as $verb) {
  1421. if (!preg_match('/'.self::VERBS.'/',$verb))
  1422. $this->error(501,$verb.' '.$this->hive['URI']);
  1423. $this->hive['ROUTES'][$parts[3]][$type][strtoupper($verb)]=
  1424. [$handler,$ttl,$kbps,$alias];
  1425. }
  1426. }
  1427. /**
  1428. * Reroute to specified URI
  1429. * @return NULL
  1430. * @param $url array|string
  1431. * @param $permanent bool
  1432. * @param $die bool
  1433. **/
  1434. function reroute($url=NULL,$permanent=FALSE,$die=TRUE) {
  1435. if (!$url)
  1436. $url=$this->hive['REALM'];
  1437. if (is_array($url))
  1438. $url=call_user_func_array([$this,'alias'],$url);
  1439. elseif (preg_match('/^(?:@([^\/()?#]+)(?:\((.+?)\))*(\?[^#]+)*(#.+)*)/',
  1440. $url,$parts) && isset($this->hive['ALIASES'][$parts[1]]))
  1441. $url=$this->build($this->hive['ALIASES'][$parts[1]],
  1442. isset($parts[2])?$this->parse($parts[2]):[]).
  1443. (isset($parts[3])?$parts[3]:'').(isset($parts[4])?$parts[4]:'');
  1444. else
  1445. $url=$this->build($url);
  1446. if (($handler=$this->hive['ONREROUTE']) &&
  1447. $this->call($handler,[$url,$permanent,$die])!==FALSE)
  1448. return;
  1449. if ($url[0]!='/' && !preg_match('/^\w+:\/\//i',$url))
  1450. $url='/'.$url;
  1451. if ($url[0]=='/' && (empty($url[1]) || $url[1]!='/')) {
  1452. $port=$this->hive['PORT'];
  1453. $port=in_array($port,[80,443])?'':(':'.$port);
  1454. $url=$this->hive['SCHEME'].'://'.
  1455. $this->hive['HOST'].$port.$this->hive['BASE'].$url;
  1456. }
  1457. if ($this->hive['CLI'])
  1458. $this->mock('GET '.$url.' [cli]');
  1459. else {
  1460. header('Location: '.$url);
  1461. $this->status($permanent?301:302);
  1462. if ($die)
  1463. die;
  1464. }
  1465. }
  1466. /**
  1467. * Provide ReST interface by mapping HTTP verb to class method
  1468. * @return NULL
  1469. * @param $url string
  1470. * @param $class string|object
  1471. * @param $ttl int
  1472. * @param $kbps int
  1473. **/
  1474. function map($url,$class,$ttl=0,$kbps=0) {
  1475. if (is_array($url)) {
  1476. foreach ($url as $item)
  1477. $this->map($item,$class,$ttl,$kbps);
  1478. return;
  1479. }
  1480. foreach (explode('|',self::VERBS) as $method)
  1481. $this->route($method.' '.$url,is_string($class)?
  1482. $class.'->'.$this->hive['PREMAP'].strtolower($method):
  1483. [$class,$this->hive['PREMAP'].strtolower($method)],
  1484. $ttl,$kbps);
  1485. }
  1486. /**
  1487. * Redirect a route to another URL
  1488. * @return NULL
  1489. * @param $pattern string|array
  1490. * @param $url string
  1491. * @param $permanent bool
  1492. */
  1493. function redirect($pattern,$url,$permanent=TRUE) {
  1494. if (is_array($pattern)) {
  1495. foreach ($pattern as $item)
  1496. $this->redirect($item,$url,$permanent);
  1497. return;
  1498. }
  1499. $this->route($pattern,function($fw) use($url,$permanent) {
  1500. $fw->reroute($url,$permanent);
  1501. });
  1502. }
  1503. /**
  1504. * Return TRUE if IPv4 address exists in DNSBL
  1505. * @return bool
  1506. * @param $ip string
  1507. **/
  1508. function blacklisted($ip) {
  1509. if ($this->hive['DNSBL'] &&
  1510. !in_array($ip,
  1511. is_array($this->hive['EXEMPT'])?
  1512. $this->hive['EXEMPT']:
  1513. $this->split($this->hive['EXEMPT']))) {
  1514. // Reverse IPv4 dotted quad
  1515. $rev=implode('.',array_reverse(explode('.',$ip)));
  1516. foreach (is_array($this->hive['DNSBL'])?
  1517. $this->hive['DNSBL']:
  1518. $this->split($this->hive['DNSBL']) as $server)
  1519. // DNSBL lookup
  1520. if (checkdnsrr($rev.'.'.$server,'A'))
  1521. return TRUE;
  1522. }
  1523. return FALSE;
  1524. }
  1525. /**
  1526. * Applies the specified URL mask and returns parameterized matches
  1527. * @return $args array
  1528. * @param $pattern string
  1529. * @param $url string|NULL
  1530. **/
  1531. function mask($pattern,$url=NULL) {
  1532. if (!$url)
  1533. $url=$this->rel($this->hive['URI']);
  1534. $case=$this->hive['CASELESS']?'i':'';
  1535. $wild=preg_quote($pattern,'/');
  1536. $i=0;
  1537. while (is_int($pos=strpos($wild,'\*'))) {
  1538. $wild=substr_replace($wild,'(?P<_'.$i.'>[^\?]*)',$pos,2);
  1539. $i++;
  1540. }
  1541. preg_match('/^'.
  1542. preg_replace(
  1543. '/((\\\{)?@(\w+\b)(?(2)\\\}))/',
  1544. '(?P<\3>[^\/\?]+)',
  1545. $wild).'\/?$/'.$case.'um',$url,$args);
  1546. foreach (array_keys($args) as $key) {
  1547. if (preg_match('/^_\d+$/',$key)) {
  1548. if (empty($args['*']))
  1549. $args['*']=$args[$key];
  1550. else {
  1551. if (is_string($args['*']))
  1552. $args['*']=[$args['*']];
  1553. array_push($args['*'],$args[$key]);
  1554. }
  1555. unset($args[$key]);
  1556. }
  1557. elseif (is_numeric($key) && $key)
  1558. unset($args[$key]);
  1559. }
  1560. return $args;
  1561. }
  1562. /**
  1563. * Match routes against incoming URI
  1564. * @return mixed
  1565. **/
  1566. function run() {
  1567. if ($this->blacklisted($this->hive['IP']))
  1568. // Spammer detected
  1569. $this->error(403);
  1570. if (!$this->hive['ROUTES'])
  1571. // No routes defined
  1572. user_error(self::E_Routes,E_USER_ERROR);
  1573. // Match specific routes first
  1574. $paths=[];
  1575. foreach ($keys=array_keys($this->hive['ROUTES']) as $key) {
  1576. $path=preg_replace('/@\w+/','*@',$key);
  1577. if (substr($path,-1)!='*')
  1578. $path.='+';
  1579. $paths[]=$path;
  1580. }
  1581. $vals=array_values($this->hive['ROUTES']);
  1582. array_multisort($paths,SORT_DESC,$keys,$vals);
  1583. $this->hive['ROUTES']=array_combine($keys,$vals);
  1584. // Convert to BASE-relative URL
  1585. $req=urldecode($this->hive['PATH']);
  1586. $preflight=FALSE;
  1587. if ($cors=(isset($this->hive['HEADERS']['Origin']) &&
  1588. $this->hive['CORS']['origin'])) {
  1589. $cors=$this->hive['CORS'];
  1590. header('Access-Control-Allow-Origin: '.$cors['origin']);
  1591. header('Access-Control-Allow-Credentials: '.
  1592. $this->export($cors['credentials']));
  1593. $preflight=
  1594. isset($this->hive['HEADERS']['Access-Control-Request-Method']);
  1595. }
  1596. $allowed=[];
  1597. foreach ($this->hive['ROUTES'] as $pattern=>$routes) {
  1598. if (!$args=$this->mask($pattern,$req))
  1599. continue;
  1600. ksort($args);
  1601. $route=NULL;
  1602. $ptr=$this->hive['CLI']?self::REQ_CLI:$this->hive['AJAX']+1;
  1603. if (isset($routes[$ptr][$this->hive['VERB']]) ||
  1604. isset($routes[$ptr=0]))
  1605. $route=$routes[$ptr];
  1606. if (!$route)
  1607. continue;
  1608. if (isset($route[$this->hive['VERB']]) && !$preflight) {
  1609. if ($this->hive['VERB']=='GET' &&
  1610. preg_match('/.+\/$/',$this->hive['PATH']))
  1611. $this->reroute(substr($this->hive['PATH'],0,-1).
  1612. ($this->hive['QUERY']?('?'.$this->hive['QUERY']):''));
  1613. list($handler,$ttl,$kbps,$alias)=$route[$this->hive['VERB']];
  1614. // Capture values of route pattern tokens
  1615. $this->hive['PARAMS']=$args;
  1616. // Save matching route
  1617. $this->hive['ALIAS']=$alias;
  1618. $this->hive['PATTERN']=$pattern;
  1619. if ($cors && $cors['expose'])
  1620. header('Access-Control-Expose-Headers: '.
  1621. (is_array($cors['expose'])?
  1622. implode(',',$cors['expose']):$cors['expose']));
  1623. if (is_string($handler)) {
  1624. // Replace route pattern tokens in handler if any
  1625. $handler=preg_replace_callback('/({)?@(\w+\b)(?(1)})/',
  1626. function($id) use($args) {
  1627. $pid=count($id)>2?2:1;
  1628. return isset($args[$id[$pid]])?
  1629. $args[$id[$pid]]:
  1630. $id[0];
  1631. },
  1632. $handler
  1633. );
  1634. if (preg_match('/(.+)\h*(?:->|::)/',$handler,$match) &&
  1635. !class_exists($match[1]))
  1636. $this->error(404);
  1637. }
  1638. // Process request
  1639. $result=NULL;
  1640. $body='';
  1641. $now=microtime(TRUE);
  1642. if (preg_match('/GET|HEAD/',$this->hive['VERB']) && $ttl) {
  1643. // Only GET and HEAD requests are cacheable
  1644. $headers=$this->hive['HEADERS'];
  1645. $cache=Cache::instance();
  1646. $cached=$cache->exists(
  1647. $hash=$this->hash($this->hive['VERB'].' '.
  1648. $this->hive['URI']).'.url',$data);
  1649. if ($cached) {
  1650. if (isset($headers['If-Modified-Since']) &&
  1651. strtotime($headers['If-Modified-Since'])+
  1652. $ttl>$now) {
  1653. $this->status(304);
  1654. die;
  1655. }
  1656. // Retrieve from cache backend
  1657. list($headers,$body,$result)=$data;
  1658. if (!$this->hive['CLI'])
  1659. array_walk($headers,'header');
  1660. $this->expire($cached[0]+$ttl-$now);
  1661. }
  1662. else
  1663. // Expire HTTP client-cached page
  1664. $this->expire($ttl);
  1665. }
  1666. else
  1667. $this->expire(0);
  1668. if (!strlen($body)) {
  1669. if (!$this->hive['RAW'] && !$this->hive['BODY'])
  1670. $this->hive['BODY']=file_get_contents('php://input');
  1671. ob_start();
  1672. // Call route handler
  1673. $result=$this->call($handler,[$this,$args,$handler],
  1674. 'beforeroute,afterroute');
  1675. $body=ob_get_clean();
  1676. if (isset($cache) && !error_get_last()) {
  1677. // Save to cache backend
  1678. $cache->set($hash,[
  1679. // Remove cookies
  1680. preg_grep('/Set-Cookie\:/',headers_list(),
  1681. PREG_GREP_INVERT),$body,$result],$ttl);
  1682. }
  1683. }
  1684. $this->hive['RESPONSE']=$body;
  1685. if (!$this->hive['QUIET']) {
  1686. if ($kbps) {
  1687. $ctr=0;
  1688. foreach (str_split($body,1024) as $part) {
  1689. // Throttle output
  1690. $ctr++;
  1691. if ($ctr/$kbps>($elapsed=microtime(TRUE)-$now) &&
  1692. !connection_aborted())
  1693. usleep(1e6*($ctr/$kbps-$elapsed));
  1694. echo $part;
  1695. }
  1696. }
  1697. else
  1698. echo $body;
  1699. }
  1700. if ($result || $this->hive['VERB']!='OPTIONS')
  1701. return $result;
  1702. }
  1703. $allowed=array_merge($allowed,array_keys($route));
  1704. }
  1705. if (!$allowed)
  1706. // URL doesn't match any route
  1707. $this->error(404);
  1708. elseif (!$this->hive['CLI']) {
  1709. if (!preg_grep('/Allow:/',$headers_send=headers_list()))
  1710. // Unhandled HTTP method
  1711. header('Allow: '.implode(',',array_unique($allowed)));
  1712. if ($cors) {
  1713. if (!preg_grep('/Access-Control-Allow-Methods:/',$headers_send))
  1714. header('Access-Control-Allow-Methods: OPTIONS,'.
  1715. implode(',',$allowed));
  1716. if ($cors['headers'] &&
  1717. !preg_grep('/Access-Control-Allow-Headers:/',$headers_send))
  1718. header('Access-Control-Allow-Headers: '.
  1719. (is_array($cors['headers'])?
  1720. implode(',',$cors['headers']):
  1721. $cors['headers']));
  1722. if ($cors['ttl']>0)
  1723. header('Access-Control-Max-Age: '.$cors['ttl']);
  1724. }
  1725. if ($this->hive['VERB']!='OPTIONS')
  1726. $this->error(405);
  1727. }
  1728. return FALSE;
  1729. }
  1730. /**
  1731. * Loop until callback returns TRUE (for long polling)
  1732. * @return mixed
  1733. * @param $func callback
  1734. * @param $args array
  1735. * @param $timeout int
  1736. **/
  1737. function until($func,$args=NULL,$timeout=60) {
  1738. if (!$args)
  1739. $args=[];
  1740. $time=time();
  1741. $max=ini_get('max_execution_time');
  1742. $limit=max(0,($max?min($timeout,$max):$timeout)-1);
  1743. $out='';
  1744. // Turn output buffering on
  1745. ob_start();
  1746. // Not for the weak of heart
  1747. while (
  1748. // No error occurred
  1749. !$this->hive['ERROR'] &&
  1750. // Got time left?
  1751. time()-$time+1<$limit &&
  1752. // Still alive?
  1753. !connection_aborted() &&
  1754. // Restart session
  1755. !headers_sent() &&
  1756. (session_status()==PHP_SESSION_ACTIVE || session_start()) &&
  1757. // CAUTION: Callback will kill host if it never becomes truthy!
  1758. !$out=$this->call($func,$args)) {
  1759. if (!$this->hive['CLI'])
  1760. session_commit();
  1761. // Hush down
  1762. sleep(1);
  1763. }
  1764. ob_flush();
  1765. flush();
  1766. return $out;
  1767. }
  1768. /**
  1769. * Disconnect HTTP client;
  1770. * Set FcgidOutputBufferSize to zero if server uses mod_fcgid;
  1771. * Disable mod_deflate when rendering text/html output
  1772. **/
  1773. function abort() {
  1774. if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
  1775. session_start();
  1776. $out='';
  1777. while (ob_get_level())
  1778. $out=ob_get_clean().$out;
  1779. if (!headers_sent()) {
  1780. header('Content-Length: '.strlen($out));
  1781. header('Connection: close');
  1782. }
  1783. session_commit();
  1784. echo $out;
  1785. flush();
  1786. if (function_exists('fastcgi_finish_request'))
  1787. fastcgi_finish_request();
  1788. }
  1789. /**
  1790. * Grab the real route handler behind the string expression
  1791. * @return string|array
  1792. * @param $func string
  1793. * @param $args array
  1794. **/
  1795. function grab($func,$args=NULL) {
  1796. if (preg_match('/(.+)\h*(->|::)\h*(.+)/s',$func,$parts)) {
  1797. // Convert string to executable PHP callback
  1798. if (!class_exists($parts[1]))
  1799. user_error(sprintf(self::E_Class,$parts[1]),E_USER_ERROR);
  1800. if ($parts[2]=='->') {
  1801. if (is_subclass_of($parts[1],'Prefab'))
  1802. $parts[1]=call_user_func($parts[1].'::instance');
  1803. elseif (isset($this->hive['CONTAINER'])) {
  1804. $container=$this->hive['CONTAINER'];
  1805. if (is_object($container) && is_callable([$container,'has'])
  1806. && $container->has($parts[1])) // PSR11
  1807. $parts[1]=call_user_func([$container,'get'],$parts[1]);
  1808. elseif (is_callable($container))
  1809. $parts[1]=call_user_func($container,$parts[1],$args);
  1810. elseif (is_string($container) &&
  1811. is_subclass_of($container,'Prefab'))
  1812. $parts[1]=call_user_func($container.'::instance')->
  1813. get($parts[1]);
  1814. else
  1815. user_error(sprintf(self::E_Class,
  1816. $this->stringify($parts[1])),
  1817. E_USER_ERROR);
  1818. }
  1819. else {
  1820. $ref=new ReflectionClass($parts[1]);
  1821. $parts[1]=method_exists($parts[1],'__construct') && $args?
  1822. $ref->newinstanceargs($args):
  1823. $ref->newinstance();
  1824. }
  1825. }
  1826. $func=[$parts[1],$parts[3]];
  1827. }
  1828. return $func;
  1829. }
  1830. /**
  1831. * Execute callback/hooks (supports 'class->method' format)
  1832. * @return mixed|FALSE
  1833. * @param $func callback
  1834. * @param $args mixed
  1835. * @param $hooks string
  1836. **/
  1837. function call($func,$args=NULL,$hooks='') {
  1838. if (!is_array($args))
  1839. $args=[$args];
  1840. // Grab the real handler behind the string representation
  1841. if (is_string($func))
  1842. $func=$this->grab($func,$args);
  1843. // Execute function; abort if callback/hook returns FALSE
  1844. if (!is_callable($func))
  1845. // No route handler
  1846. if ($hooks=='beforeroute,afterroute') {
  1847. $allowed=[];
  1848. if (is_array($func))
  1849. $allowed=array_intersect(
  1850. array_map('strtoupper',get_class_methods($func[0])),
  1851. explode('|',self::VERBS)
  1852. );
  1853. header('Allow: '.implode(',',$allowed));
  1854. $this->error(405);
  1855. }
  1856. else
  1857. user_error(sprintf(self::E_Method,
  1858. is_string($func)?$func:$this->stringify($func)),
  1859. E_USER_ERROR);
  1860. $obj=FALSE;
  1861. if (is_array($func)) {
  1862. $hooks=$this->split($hooks);
  1863. $obj=TRUE;
  1864. }
  1865. // Execute pre-route hook if any
  1866. if ($obj && $hooks && in_array($hook='beforeroute',$hooks) &&
  1867. method_exists($func[0],$hook) &&
  1868. call_user_func_array([$func[0],$hook],$args)===FALSE)
  1869. return FALSE;
  1870. // Execute callback
  1871. $out=call_user_func_array($func,$args?:[]);
  1872. if ($out===FALSE)
  1873. return FALSE;
  1874. // Execute post-route hook if any
  1875. if ($obj && $hooks && in_array($hook='afterroute',$hooks) &&
  1876. method_exists($func[0],$hook) &&
  1877. call_user_func_array([$func[0],$hook],$args)===FALSE)
  1878. return FALSE;
  1879. return $out;
  1880. }
  1881. /**
  1882. * Execute specified callbacks in succession; Apply same arguments
  1883. * to all callbacks
  1884. * @return array
  1885. * @param $funcs array|string
  1886. * @param $args mixed
  1887. **/
  1888. function chain($funcs,$args=NULL) {
  1889. $out=[];
  1890. foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func)
  1891. $out[]=$this->call($func,$args);
  1892. return $out;
  1893. }
  1894. /**
  1895. * Execute specified callbacks in succession; Relay result of
  1896. * previous callback as argument to the next callback
  1897. * @return array
  1898. * @param $funcs array|string
  1899. * @param $args mixed
  1900. **/
  1901. function relay($funcs,$args=NULL) {
  1902. foreach (is_array($funcs)?$funcs:$this->split($funcs) as $func)
  1903. $args=[$this->call($func,$args)];
  1904. return array_shift($args);
  1905. }
  1906. /**
  1907. * Configure framework according to .ini-style file settings;
  1908. * If optional 2nd arg is provided, template strings are interpreted
  1909. * @return object
  1910. * @param $source string|array
  1911. * @param $allow bool
  1912. **/
  1913. function config($source,$allow=FALSE) {
  1914. if (is_string($source))
  1915. $source=$this->split($source);
  1916. if ($allow)
  1917. $preview=Preview::instance();
  1918. foreach ($source as $file) {
  1919. preg_match_all(
  1920. '/(?<=^|\n)(?:'.
  1921. '\[(?<section>.+?)\]|'.
  1922. '(?<lval>[^\h\r\n;].*?)\h*=\h*'.
  1923. '(?<rval>(?:\\\\\h*\r?\n|.+?)*)'.
  1924. ')(?=\r?\n|$)/',
  1925. $this->read($file),
  1926. $matches,PREG_SET_ORDER);
  1927. if ($matches) {
  1928. $sec='globals';
  1929. $cmd=[];
  1930. foreach ($matches as $match) {
  1931. if ($match['section']) {
  1932. $sec=$match['section'];
  1933. if (preg_match(
  1934. '/^(?!(?:global|config|route|map|redirect)s\b)'.
  1935. '(.*?)(?:\s*[:>])/i',$sec,$msec) &&
  1936. !$this->exists($msec[1]))
  1937. $this->set($msec[1],NULL);
  1938. preg_match('/^(config|route|map|redirect)s\b|'.
  1939. '^(.+?)\s*\>\s*(.*)/i',$sec,$cmd);
  1940. continue;
  1941. }
  1942. if ($allow)
  1943. foreach (['lval','rval'] as $ndx)
  1944. $match[$ndx]=$preview->
  1945. resolve($match[$ndx],NULL,0,FALSE,FALSE);
  1946. if (!empty($cmd)) {
  1947. isset($cmd[3])?
  1948. $this->call($cmd[3],
  1949. [$match['lval'],$match['rval'],$cmd[2]]):
  1950. call_user_func_array(
  1951. [$this,$cmd[1]],
  1952. array_merge([$match['lval']],
  1953. str_getcsv($cmd[1]=='config'?
  1954. $this->cast($match['rval']):
  1955. $match['rval']))
  1956. );
  1957. }
  1958. else {
  1959. $rval=preg_replace(
  1960. '/\\\\\h*(\r?\n)/','\1',$match['rval']);
  1961. $ttl=NULL;
  1962. if (preg_match('/^(.+)\|\h*(\d+)$/',$rval,$tmp)) {
  1963. array_shift($tmp);
  1964. list($rval,$ttl)=$tmp;
  1965. }
  1966. $args=array_map(
  1967. function($val) {
  1968. $val=$this->cast($val);
  1969. if (is_string($val))
  1970. $val=strlen($val)?
  1971. preg_replace('/\\\\"/','"',$val):
  1972. NULL;
  1973. return $val;
  1974. },
  1975. // Mark quoted strings with 0x00 whitespace
  1976. str_getcsv(preg_replace(
  1977. '/(?<!\\\\)(")(.*?)\1/',
  1978. "\\1\x00\\2\\1",trim($rval)))
  1979. );
  1980. preg_match('/^(?<section>[^:]+)(?:\:(?<func>.+))?/',
  1981. $sec,$parts);
  1982. $func=isset($parts['func'])?$parts['func']:NULL;
  1983. $custom=(strtolower($parts['section'])!='globals');
  1984. if ($func)
  1985. $args=[$this->call($func,$args)];
  1986. if (count($args)>1)
  1987. $args=[$args];
  1988. if (isset($ttl))
  1989. $args=array_merge($args,[$ttl]);
  1990. call_user_func_array(
  1991. [$this,'set'],
  1992. array_merge(
  1993. [
  1994. ($custom?($parts['section'].'.'):'').
  1995. $match['lval']
  1996. ],
  1997. $args
  1998. )
  1999. );
  2000. }
  2001. }
  2002. }
  2003. }
  2004. return $this;
  2005. }
  2006. /**
  2007. * Create mutex, invoke callback then drop ownership when done
  2008. * @return mixed
  2009. * @param $id string
  2010. * @param $func callback
  2011. * @param $args mixed
  2012. **/
  2013. function mutex($id,$func,$args=NULL) {
  2014. if (!is_dir($tmp=$this->hive['TEMP']))
  2015. mkdir($tmp,self::MODE,TRUE);
  2016. // Use filesystem lock
  2017. if (is_file($lock=$tmp.
  2018. $this->hive['SEED'].'.'.$this->hash($id).'.lock') &&
  2019. filemtime($lock)+ini_get('max_execution_time')<microtime(TRUE))
  2020. // Stale lock
  2021. @unlink($lock);
  2022. while (!($handle=@fopen($lock,'x')) && !connection_aborted())
  2023. usleep(mt_rand(0,100));
  2024. $this->locks[$id]=$lock;
  2025. $out=$this->call($func,$args);
  2026. fclose($handle);
  2027. @unlink($lock);
  2028. unset($this->locks[$id]);
  2029. return $out;
  2030. }
  2031. /**
  2032. * Read file (with option to apply Unix LF as standard line ending)
  2033. * @return string
  2034. * @param $file string
  2035. * @param $lf bool
  2036. **/
  2037. function read($file,$lf=FALSE) {
  2038. $out=@file_get_contents($file);
  2039. return $lf?preg_replace('/\r\n|\r/',"\n",$out):$out;
  2040. }
  2041. /**
  2042. * Exclusive file write
  2043. * @return int|FALSE
  2044. * @param $file string
  2045. * @param $data mixed
  2046. * @param $append bool
  2047. **/
  2048. function write($file,$data,$append=FALSE) {
  2049. return file_put_contents($file,$data,$this->hive['LOCK']|($append?FILE_APPEND:0));
  2050. }
  2051. /**
  2052. * Apply syntax highlighting
  2053. * @return string
  2054. * @param $text string
  2055. **/
  2056. function highlight($text) {
  2057. $out='';
  2058. $pre=FALSE;
  2059. $text=trim($text);
  2060. if ($text && !preg_match('/^<\?php/',$text)) {
  2061. $text='<?php '.$text;
  2062. $pre=TRUE;
  2063. }
  2064. foreach (token_get_all($text) as $token)
  2065. if ($pre)
  2066. $pre=FALSE;
  2067. else
  2068. $out.='<span'.
  2069. (is_array($token)?
  2070. (' class="'.
  2071. substr(strtolower(token_name($token[0])),2).'">'.
  2072. $this->encode($token[1]).''):
  2073. ('>'.$this->encode($token))).
  2074. '</span>';
  2075. return $out?('<code>'.$out.'</code>'):$text;
  2076. }
  2077. /**
  2078. * Dump expression with syntax highlighting
  2079. * @param $expr mixed
  2080. **/
  2081. function dump($expr) {
  2082. echo $this->highlight($this->stringify($expr));
  2083. }
  2084. /**
  2085. * Return path (and query parameters) relative to the base directory
  2086. * @return string
  2087. * @param $url string
  2088. **/
  2089. function rel($url) {
  2090. return preg_replace('/^(?:https?:\/\/)?'.
  2091. preg_quote($this->hive['BASE'],'/').'(\/.*|$)/','\1',$url);
  2092. }
  2093. /**
  2094. * Namespace-aware class autoloader
  2095. * @return mixed
  2096. * @param $class string
  2097. **/
  2098. protected function autoload($class) {
  2099. $class=$this->fixslashes(ltrim($class,'\\'));
  2100. /** @var callable $func */
  2101. $func=NULL;
  2102. if (is_array($path=$this->hive['AUTOLOAD']) &&
  2103. isset($path[1]) && is_callable($path[1]))
  2104. list($path,$func)=$path;
  2105. foreach ($this->split($this->hive['PLUGINS'].';'.$path) as $auto)
  2106. if ($func && is_file($file=$func($auto.$class).'.php') ||
  2107. is_file($file=$auto.$class.'.php') ||
  2108. is_file($file=$auto.strtolower($class).'.php') ||
  2109. is_file($file=strtolower($auto.$class).'.php'))
  2110. return require($file);
  2111. }
  2112. /**
  2113. * Execute framework/application shutdown sequence
  2114. * @param $cwd string
  2115. **/
  2116. function unload($cwd) {
  2117. chdir($cwd);
  2118. if (!($error=error_get_last()) &&
  2119. session_status()==PHP_SESSION_ACTIVE)
  2120. session_commit();
  2121. foreach ($this->locks as $lock)
  2122. @unlink($lock);
  2123. $handler=$this->hive['UNLOAD'];
  2124. if ((!$handler || $this->call($handler,$this)===FALSE) &&
  2125. $error && in_array($error['type'],
  2126. [E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR]))
  2127. // Fatal error detected
  2128. $this->error(500,
  2129. sprintf(self::E_Fatal,$error['message']),[$error]);
  2130. }
  2131. /**
  2132. * Convenience method for checking hive key
  2133. * @return mixed
  2134. * @param $key string
  2135. **/
  2136. function offsetexists($key) {
  2137. return $this->exists($key);
  2138. }
  2139. /**
  2140. * Convenience method for assigning hive value
  2141. * @return mixed
  2142. * @param $key string
  2143. * @param $val mixed
  2144. **/
  2145. function offsetset($key,$val) {
  2146. return $this->set($key,$val);
  2147. }
  2148. /**
  2149. * Convenience method for retrieving hive value
  2150. * @return mixed
  2151. * @param $key string
  2152. **/
  2153. function &offsetget($key) {
  2154. $val=&$this->ref($key);
  2155. return $val;
  2156. }
  2157. /**
  2158. * Convenience method for removing hive key
  2159. * @param $key string
  2160. **/
  2161. function offsetunset($key) {
  2162. $this->clear($key);
  2163. }
  2164. /**
  2165. * Alias for offsetexists()
  2166. * @return mixed
  2167. * @param $key string
  2168. **/
  2169. function __isset($key) {
  2170. return $this->offsetexists($key);
  2171. }
  2172. /**
  2173. * Alias for offsetset()
  2174. * @return mixed
  2175. * @param $key string
  2176. * @param $val mixed
  2177. **/
  2178. function __set($key,$val) {
  2179. return $this->offsetset($key,$val);
  2180. }
  2181. /**
  2182. * Alias for offsetget()
  2183. * @return mixed
  2184. * @param $key string
  2185. **/
  2186. function &__get($key) {
  2187. $val=&$this->offsetget($key);
  2188. return $val;
  2189. }
  2190. /**
  2191. * Alias for offsetunset()
  2192. * @param $key string
  2193. **/
  2194. function __unset($key) {
  2195. $this->offsetunset($key);
  2196. }
  2197. /**
  2198. * Call function identified by hive key
  2199. * @return mixed
  2200. * @param $key string
  2201. * @param $args array
  2202. **/
  2203. function __call($key,array $args) {
  2204. if ($this->exists($key,$val))
  2205. return call_user_func_array($val,$args);
  2206. user_error(sprintf(self::E_Method,$key),E_USER_ERROR);
  2207. }
  2208. //! Prohibit cloning
  2209. private function __clone() {
  2210. }
  2211. //! Bootstrap
  2212. function __construct() {
  2213. // Managed directives
  2214. ini_set('default_charset',$charset='UTF-8');
  2215. if (extension_loaded('mbstring'))
  2216. mb_internal_encoding($charset);
  2217. ini_set('display_errors',0);
  2218. // Deprecated directives
  2219. @ini_set('magic_quotes_gpc',0);
  2220. @ini_set('register_globals',0);
  2221. // Intercept errors/exceptions; PHP5.3-compatible
  2222. $check=error_reporting((E_ALL|E_STRICT)&~(E_NOTICE|E_USER_NOTICE));
  2223. set_exception_handler(
  2224. function($obj) {
  2225. /** @var Exception $obj */
  2226. $this->hive['EXCEPTION']=$obj;
  2227. $this->error(500,
  2228. $obj->getmessage().' '.
  2229. '['.$obj->getFile().':'.$obj->getLine().']',
  2230. $obj->gettrace());
  2231. }
  2232. );
  2233. set_error_handler(
  2234. function($level,$text,$file,$line) {
  2235. if ($level & error_reporting())
  2236. $this->error(500,$text,NULL,$level);
  2237. }
  2238. );
  2239. if (!isset($_SERVER['SERVER_NAME']) || $_SERVER['SERVER_NAME']==='')
  2240. $_SERVER['SERVER_NAME']=gethostname();
  2241. $headers=[];
  2242. if ($cli=PHP_SAPI=='cli') {
  2243. // Emulate HTTP request
  2244. $_SERVER['REQUEST_METHOD']='GET';
  2245. if (!isset($_SERVER['argv'][1])) {
  2246. $_SERVER['argc']++;
  2247. $_SERVER['argv'][1]='/';
  2248. }
  2249. $req=$query='';
  2250. if (substr($_SERVER['argv'][1],0,1)=='/') {
  2251. $req=$_SERVER['argv'][1];
  2252. $query=parse_url($req,PHP_URL_QUERY);
  2253. } else {
  2254. foreach($_SERVER['argv'] as $i=>$arg) {
  2255. if (!$i) continue;
  2256. if (preg_match('/^\-(\-)?(\w+)(?:\=(.*))?$/',$arg,$m)) {
  2257. foreach($m[1]?[$m[2]]:str_split($m[2]) as $k)
  2258. $query.=($query?'&':'').urlencode($k).'=';
  2259. if (isset($m[3]))
  2260. $query.=urlencode($m[3]);
  2261. } else
  2262. $req.='/'.$arg;
  2263. }
  2264. if (!$req)
  2265. $req='/';
  2266. if ($query)
  2267. $req.='?'.$query;
  2268. }
  2269. $_SERVER['REQUEST_URI']=$req;
  2270. parse_str($query,$GLOBALS['_GET']);
  2271. }
  2272. elseif (function_exists('getallheaders')) {
  2273. foreach (getallheaders() as $key=>$val) {
  2274. $tmp=strtoupper(strtr($key,'-','_'));
  2275. // TODO: use ucwords delimiters for php 5.4.32+ & 5.5.16+
  2276. $key=strtr(ucwords(strtolower(strtr($key,'-',' '))),' ','-');
  2277. $headers[$key]=$val;
  2278. if (isset($_SERVER['HTTP_'.$tmp]))
  2279. $headers[$key]=&$_SERVER['HTTP_'.$tmp];
  2280. }
  2281. }
  2282. else {
  2283. if (isset($_SERVER['CONTENT_LENGTH']))
  2284. $headers['Content-Length']=&$_SERVER['CONTENT_LENGTH'];
  2285. if (isset($_SERVER['CONTENT_TYPE']))
  2286. $headers['Content-Type']=&$_SERVER['CONTENT_TYPE'];
  2287. foreach (array_keys($_SERVER) as $key)
  2288. if (substr($key,0,5)=='HTTP_')
  2289. $headers[strtr(ucwords(strtolower(strtr(
  2290. substr($key,5),'_',' '))),' ','-')]=&$_SERVER[$key];
  2291. }
  2292. if (isset($headers['X-HTTP-Method-Override']))
  2293. $_SERVER['REQUEST_METHOD']=$headers['X-HTTP-Method-Override'];
  2294. elseif ($_SERVER['REQUEST_METHOD']=='POST' && isset($_POST['_method']))
  2295. $_SERVER['REQUEST_METHOD']=strtoupper($_POST['_method']);
  2296. $scheme=isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']=='on' ||
  2297. isset($headers['X-Forwarded-Proto']) &&
  2298. $headers['X-Forwarded-Proto']=='https'?'https':'http';
  2299. // Create hive early on to expose header methods
  2300. $this->hive=['HEADERS'=>&$headers];
  2301. if (function_exists('apache_setenv')) {
  2302. // Work around Apache pre-2.4 VirtualDocumentRoot bug
  2303. $_SERVER['DOCUMENT_ROOT']=str_replace($_SERVER['SCRIPT_NAME'],'',
  2304. $_SERVER['SCRIPT_FILENAME']);
  2305. apache_setenv("DOCUMENT_ROOT",$_SERVER['DOCUMENT_ROOT']);
  2306. }
  2307. $_SERVER['DOCUMENT_ROOT']=realpath($_SERVER['DOCUMENT_ROOT']);
  2308. $base='';
  2309. if (!$cli)
  2310. $base=rtrim($this->fixslashes(
  2311. dirname($_SERVER['SCRIPT_NAME'])),'/');
  2312. $uri=parse_url((preg_match('/^\w+:\/\//',$_SERVER['REQUEST_URI'])?'':
  2313. $scheme.'://'.$_SERVER['SERVER_NAME']).$_SERVER['REQUEST_URI']);
  2314. $_SERVER['REQUEST_URI']=$uri['path'].
  2315. (isset($uri['query'])?'?'.$uri['query']:'').
  2316. (isset($uri['fragment'])?'#'.$uri['fragment']:'');
  2317. $path=preg_replace('/^'.preg_quote($base,'/').'/','',$uri['path']);
  2318. $jar=[
  2319. 'expire'=>0,
  2320. 'lifetime'=>0,
  2321. 'path'=>$base?:'/',
  2322. 'domain'=>is_int(strpos($_SERVER['SERVER_NAME'],'.')) &&
  2323. !filter_var($_SERVER['SERVER_NAME'],FILTER_VALIDATE_IP)?
  2324. $_SERVER['SERVER_NAME']:'',
  2325. 'secure'=>($scheme=='https'),
  2326. 'httponly'=>TRUE,
  2327. 'samesite'=>'Lax',
  2328. ];
  2329. $port=80;
  2330. if (isset($headers['X-Forwarded-Port']))
  2331. $port=$headers['X-Forwarded-Port'];
  2332. elseif (isset($_SERVER['SERVER_PORT']))
  2333. $port=$_SERVER['SERVER_PORT'];
  2334. // Default configuration
  2335. $this->hive+=[
  2336. 'AGENT'=>$this->agent(),
  2337. 'AJAX'=>$this->ajax(),
  2338. 'ALIAS'=>NULL,
  2339. 'ALIASES'=>[],
  2340. 'AUTOLOAD'=>'./',
  2341. 'BASE'=>$base,
  2342. 'BITMASK'=>ENT_COMPAT,
  2343. 'BODY'=>NULL,
  2344. 'CACHE'=>FALSE,
  2345. 'CASELESS'=>TRUE,
  2346. 'CLI'=>$cli,
  2347. 'CORS'=>[
  2348. 'headers'=>'',
  2349. 'origin'=>FALSE,
  2350. 'credentials'=>FALSE,
  2351. 'expose'=>FALSE,
  2352. 'ttl'=>0
  2353. ],
  2354. 'DEBUG'=>0,
  2355. 'DIACRITICS'=>[],
  2356. 'DNSBL'=>'',
  2357. 'EMOJI'=>[],
  2358. 'ENCODING'=>$charset,
  2359. 'ERROR'=>NULL,
  2360. 'ESCAPE'=>TRUE,
  2361. 'EXCEPTION'=>NULL,
  2362. 'EXEMPT'=>NULL,
  2363. 'FALLBACK'=>$this->fallback,
  2364. 'FORMATS'=>[],
  2365. 'FRAGMENT'=>isset($uri['fragment'])?$uri['fragment']:'',
  2366. 'HALT'=>TRUE,
  2367. 'HIGHLIGHT'=>FALSE,
  2368. 'HOST'=>$_SERVER['SERVER_NAME'],
  2369. 'IP'=>$this->ip(),
  2370. 'JAR'=>$jar,
  2371. 'LANGUAGE'=>isset($headers['Accept-Language'])?
  2372. $this->language($headers['Accept-Language']):
  2373. $this->fallback,
  2374. 'LOCALES'=>'./',
  2375. 'LOCK'=>LOCK_EX,
  2376. 'LOGGABLE'=>'*',
  2377. 'LOGS'=>'./',
  2378. 'MB'=>extension_loaded('mbstring'),
  2379. 'ONERROR'=>NULL,
  2380. 'ONREROUTE'=>NULL,
  2381. 'PACKAGE'=>self::PACKAGE,
  2382. 'PARAMS'=>[],
  2383. 'PATH'=>$path,
  2384. 'PATTERN'=>NULL,
  2385. 'PLUGINS'=>$this->fixslashes(__DIR__).'/',
  2386. 'PORT'=>$port,
  2387. 'PREFIX'=>NULL,
  2388. 'PREMAP'=>'',
  2389. 'QUERY'=>isset($uri['query'])?$uri['query']:'',
  2390. 'QUIET'=>FALSE,
  2391. 'RAW'=>FALSE,
  2392. 'REALM'=>$scheme.'://'.$_SERVER['SERVER_NAME'].
  2393. ($port && !in_array($port,[80,443])?(':'.$port):'').
  2394. $_SERVER['REQUEST_URI'],
  2395. 'RESPONSE'=>'',
  2396. 'ROOT'=>$_SERVER['DOCUMENT_ROOT'],
  2397. 'ROUTES'=>[],
  2398. 'SCHEME'=>$scheme,
  2399. 'SEED'=>$this->hash($_SERVER['SERVER_NAME'].$base),
  2400. 'SERIALIZER'=>extension_loaded($ext='igbinary')?$ext:'php',
  2401. 'TEMP'=>'tmp/',
  2402. 'TIME'=>&$_SERVER['REQUEST_TIME_FLOAT'],
  2403. 'TZ'=>@date_default_timezone_get(),
  2404. 'UI'=>'./',
  2405. 'UNLOAD'=>NULL,
  2406. 'UPLOADS'=>'./',
  2407. 'URI'=>&$_SERVER['REQUEST_URI'],
  2408. 'VERB'=>&$_SERVER['REQUEST_METHOD'],
  2409. 'VERSION'=>self::VERSION,
  2410. 'XFRAME'=>'SAMEORIGIN'
  2411. ];
  2412. if (!headers_sent() && session_status()!=PHP_SESSION_ACTIVE) {
  2413. unset($jar['expire']);
  2414. session_cache_limiter('');
  2415. if (version_compare(PHP_VERSION, '7.3.0') >= 0)
  2416. session_set_cookie_params($jar);
  2417. else {
  2418. unset($jar['samesite']);
  2419. call_user_func_array('session_set_cookie_params',$jar);
  2420. }
  2421. }
  2422. if (PHP_SAPI=='cli-server' &&
  2423. preg_match('/^'.preg_quote($base,'/').'$/',$this->hive['URI']))
  2424. $this->reroute('/');
  2425. if (ini_get('auto_globals_jit'))
  2426. // Override setting
  2427. $GLOBALS+=['_ENV'=>$_ENV,'_REQUEST'=>$_REQUEST];
  2428. // Sync PHP globals with corresponding hive keys
  2429. $this->init=$this->hive;
  2430. foreach (explode('|',self::GLOBALS) as $global) {
  2431. $sync=$this->sync($global);
  2432. $this->init+=[
  2433. $global=>preg_match('/SERVER|ENV/',$global)?$sync:[]
  2434. ];
  2435. }
  2436. if ($check && $error=error_get_last())
  2437. // Error detected
  2438. $this->error(500,
  2439. sprintf(self::E_Fatal,$error['message']),[$error]);
  2440. date_default_timezone_set($this->hive['TZ']);
  2441. // Register framework autoloader
  2442. spl_autoload_register([$this,'autoload']);
  2443. // Register shutdown handler
  2444. register_shutdown_function([$this,'unload'],getcwd());
  2445. }
  2446. }
  2447. //! Cache engine
  2448. class Cache extends Prefab {
  2449. protected
  2450. //! Cache DSN
  2451. $dsn,
  2452. //! Prefix for cache entries
  2453. $prefix,
  2454. //! MemCache or Redis object
  2455. $ref;
  2456. /**
  2457. * Return timestamp and TTL of cache entry or FALSE if not found
  2458. * @return array|FALSE
  2459. * @param $key string
  2460. * @param $val mixed
  2461. **/
  2462. function exists($key,&$val=NULL) {
  2463. $fw=Base::instance();
  2464. if (!$this->dsn)
  2465. return FALSE;
  2466. $ndx=$this->prefix.'.'.$key;
  2467. $parts=explode('=',$this->dsn,2);
  2468. switch ($parts[0]) {
  2469. case 'apc':
  2470. case 'apcu':
  2471. $raw=call_user_func($parts[0].'_fetch',$ndx);
  2472. break;
  2473. case 'redis':
  2474. $raw=$this->ref->get($ndx);
  2475. break;
  2476. case 'memcache':
  2477. $raw=memcache_get($this->ref,$ndx);
  2478. break;
  2479. case 'memcached':
  2480. $raw=$this->ref->get($ndx);
  2481. break;
  2482. case 'wincache':
  2483. $raw=wincache_ucache_get($ndx);
  2484. break;
  2485. case 'xcache':
  2486. $raw=xcache_get($ndx);
  2487. break;
  2488. case 'folder':
  2489. $raw=$fw->read($parts[1].$ndx);
  2490. break;
  2491. }
  2492. if (!empty($raw)) {
  2493. list($val,$time,$ttl)=(array)$fw->unserialize($raw);
  2494. if ($ttl===0 || $time+$ttl>microtime(TRUE))
  2495. return [$time,$ttl];
  2496. $val=null;
  2497. $this->clear($key);
  2498. }
  2499. return FALSE;
  2500. }
  2501. /**
  2502. * Store value in cache
  2503. * @return mixed|FALSE
  2504. * @param $key string
  2505. * @param $val mixed
  2506. * @param $ttl int
  2507. **/
  2508. function set($key,$val,$ttl=0) {
  2509. $fw=Base::instance();
  2510. if (!$this->dsn)
  2511. return TRUE;
  2512. $ndx=$this->prefix.'.'.$key;
  2513. if ($cached=$this->exists($key))
  2514. $ttl=$cached[1];
  2515. $data=$fw->serialize([$val,microtime(TRUE),$ttl]);
  2516. $parts=explode('=',$this->dsn,2);
  2517. switch ($parts[0]) {
  2518. case 'apc':
  2519. case 'apcu':
  2520. return call_user_func($parts[0].'_store',$ndx,$data,$ttl);
  2521. case 'redis':
  2522. return $this->ref->set($ndx,$data,$ttl?['ex'=>$ttl]:[]);
  2523. case 'memcache':
  2524. return memcache_set($this->ref,$ndx,$data,0,$ttl);
  2525. case 'memcached':
  2526. return $this->ref->set($ndx,$data,$ttl);
  2527. case 'wincache':
  2528. return wincache_ucache_set($ndx,$data,$ttl);
  2529. case 'xcache':
  2530. return xcache_set($ndx,$data,$ttl);
  2531. case 'folder':
  2532. return $fw->write($parts[1].
  2533. str_replace(['/','\\'],'',$ndx),$data);
  2534. }
  2535. return FALSE;
  2536. }
  2537. /**
  2538. * Retrieve value of cache entry
  2539. * @return mixed|FALSE
  2540. * @param $key string
  2541. **/
  2542. function get($key) {
  2543. return $this->dsn && $this->exists($key,$data)?$data:FALSE;
  2544. }
  2545. /**
  2546. * Delete cache entry
  2547. * @return bool
  2548. * @param $key string
  2549. **/
  2550. function clear($key) {
  2551. if (!$this->dsn)
  2552. return;
  2553. $ndx=$this->prefix.'.'.$key;
  2554. $parts=explode('=',$this->dsn,2);
  2555. switch ($parts[0]) {
  2556. case 'apc':
  2557. case 'apcu':
  2558. return call_user_func($parts[0].'_delete',$ndx);
  2559. case 'redis':
  2560. return $this->ref->del($ndx);
  2561. case 'memcache':
  2562. return memcache_delete($this->ref,$ndx);
  2563. case 'memcached':
  2564. return $this->ref->delete($ndx);
  2565. case 'wincache':
  2566. return wincache_ucache_delete($ndx);
  2567. case 'xcache':
  2568. return xcache_unset($ndx);
  2569. case 'folder':
  2570. return @unlink($parts[1].$ndx);
  2571. }
  2572. return FALSE;
  2573. }
  2574. /**
  2575. * Clear contents of cache backend
  2576. * @return bool
  2577. * @param $suffix string
  2578. **/
  2579. function reset($suffix=NULL) {
  2580. if (!$this->dsn)
  2581. return TRUE;
  2582. $regex='/'.preg_quote($this->prefix.'.','/').'.*'.
  2583. preg_quote($suffix,'/').'/';
  2584. $parts=explode('=',$this->dsn,2);
  2585. switch ($parts[0]) {
  2586. case 'apc':
  2587. case 'apcu':
  2588. $info=call_user_func($parts[0].'_cache_info',
  2589. $parts[0]=='apcu'?FALSE:'user');
  2590. if (!empty($info['cache_list'])) {
  2591. $key=array_key_exists('info',
  2592. $info['cache_list'][0])?'info':'key';
  2593. foreach ($info['cache_list'] as $item)
  2594. if (preg_match($regex,$item[$key]))
  2595. call_user_func($parts[0].'_delete',$item[$key]);
  2596. }
  2597. return TRUE;
  2598. case 'redis':
  2599. $keys=$this->ref->keys($this->prefix.'.*'.$suffix);
  2600. foreach($keys as $key)
  2601. $this->ref->del($key);
  2602. return TRUE;
  2603. case 'memcache':
  2604. foreach (memcache_get_extended_stats(
  2605. $this->ref,'slabs') as $slabs)
  2606. foreach (array_filter(array_keys($slabs),'is_numeric')
  2607. as $id)
  2608. foreach (memcache_get_extended_stats(
  2609. $this->ref,'cachedump',$id) as $data)
  2610. if (is_array($data))
  2611. foreach (array_keys($data) as $key)
  2612. if (preg_match($regex,$key))
  2613. memcache_delete($this->ref,$key);
  2614. return TRUE;
  2615. case 'memcached':
  2616. foreach ($this->ref->getallkeys()?:[] as $key)
  2617. if (preg_match($regex,$key))
  2618. $this->ref->delete($key);
  2619. return TRUE;
  2620. case 'wincache':
  2621. $info=wincache_ucache_info();
  2622. foreach ($info['ucache_entries'] as $item)
  2623. if (preg_match($regex,$item['key_name']))
  2624. wincache_ucache_delete($item['key_name']);
  2625. return TRUE;
  2626. case 'xcache':
  2627. if ($suffix && !ini_get('xcache.admin.enable_auth')) {
  2628. $cnt=xcache_count(XC_TYPE_VAR);
  2629. for ($i=0;$i<$cnt;$i++) {
  2630. $list=xcache_list(XC_TYPE_VAR,$i);
  2631. foreach ($list['cache_list'] as $item)
  2632. if (preg_match($regex,$item['name']))
  2633. xcache_unset($item['name']);
  2634. }
  2635. } else
  2636. xcache_unset_by_prefix($this->prefix.'.');
  2637. return TRUE;
  2638. case 'folder':
  2639. if ($glob=@glob($parts[1].'*'))
  2640. foreach ($glob as $file)
  2641. if (preg_match($regex,basename($file)))
  2642. @unlink($file);
  2643. return TRUE;
  2644. }
  2645. return FALSE;
  2646. }
  2647. /**
  2648. * Load/auto-detect cache backend
  2649. * @return string
  2650. * @param $dsn bool|string
  2651. * @param $seed bool|string
  2652. **/
  2653. function load($dsn,$seed=NULL) {
  2654. $fw=Base::instance();
  2655. if ($dsn=trim($dsn)) {
  2656. if (preg_match('/^redis=(.+)/',$dsn,$parts) &&
  2657. extension_loaded('redis')) {
  2658. list($host,$port,$db,$password)=explode(':',$parts[1])+[1=>6379,2=>NULL,3=>NULL];
  2659. $this->ref=new Redis;
  2660. if(!$this->ref->connect($host,$port,2))
  2661. $this->ref=NULL;
  2662. if(!empty($password))
  2663. $this->ref->auth($password);
  2664. if(isset($db))
  2665. $this->ref->select($db);
  2666. }
  2667. elseif (preg_match('/^memcache=(.+)/',$dsn,$parts) &&
  2668. extension_loaded('memcache'))
  2669. foreach ($fw->split($parts[1]) as $server) {
  2670. list($host,$port)=explode(':',$server)+[1=>11211];
  2671. if (empty($this->ref))
  2672. $this->ref=@memcache_connect($host,$port)?:NULL;
  2673. else
  2674. memcache_add_server($this->ref,$host,$port);
  2675. }
  2676. elseif (preg_match('/^memcached=(.+)/',$dsn,$parts) &&
  2677. extension_loaded('memcached'))
  2678. foreach ($fw->split($parts[1]) as $server) {
  2679. list($host,$port)=explode(':',$server)+[1=>11211];
  2680. if (empty($this->ref))
  2681. $this->ref=new Memcached();
  2682. $this->ref->addServer($host,$port);
  2683. }
  2684. if (empty($this->ref) && !preg_match('/^folder\h*=/',$dsn))
  2685. $dsn=($grep=preg_grep('/^(apc|wincache|xcache)/',
  2686. array_map('strtolower',get_loaded_extensions())))?
  2687. // Auto-detect
  2688. current($grep):
  2689. // Use filesystem as fallback
  2690. ('folder='.$fw->TEMP.'cache/');
  2691. if (preg_match('/^folder\h*=\h*(.+)/',$dsn,$parts) &&
  2692. !is_dir($parts[1]))
  2693. mkdir($parts[1],Base::MODE,TRUE);
  2694. }
  2695. $this->prefix=$seed?:$fw->SEED;
  2696. return $this->dsn=$dsn;
  2697. }
  2698. /**
  2699. * Class constructor
  2700. * @param $dsn bool|string
  2701. **/
  2702. function __construct($dsn=FALSE) {
  2703. if ($dsn)
  2704. $this->load($dsn);
  2705. }
  2706. }
  2707. //! View handler
  2708. class View extends Prefab {
  2709. private
  2710. //! Temporary hive
  2711. $temp;
  2712. protected
  2713. //! Template file
  2714. $file,
  2715. //! Post-rendering handler
  2716. $trigger,
  2717. //! Nesting level
  2718. $level=0;
  2719. /** @var \Base Framework instance */
  2720. protected $fw;
  2721. function __construct() {
  2722. $this->fw=\Base::instance();
  2723. }
  2724. /**
  2725. * Encode characters to equivalent HTML entities
  2726. * @return string
  2727. * @param $arg mixed
  2728. **/
  2729. function esc($arg) {
  2730. return $this->fw->recursive($arg,
  2731. function($val) {
  2732. return is_string($val)?$this->fw->encode($val):$val;
  2733. }
  2734. );
  2735. }
  2736. /**
  2737. * Decode HTML entities to equivalent characters
  2738. * @return string
  2739. * @param $arg mixed
  2740. **/
  2741. function raw($arg) {
  2742. return $this->fw->recursive($arg,
  2743. function($val) {
  2744. return is_string($val)?$this->fw->decode($val):$val;
  2745. }
  2746. );
  2747. }
  2748. /**
  2749. * Create sandbox for template execution
  2750. * @return string
  2751. * @param $hive array
  2752. * @param $mime string
  2753. **/
  2754. protected function sandbox(array $hive=NULL,$mime=NULL) {
  2755. $fw=$this->fw;
  2756. $implicit=FALSE;
  2757. if (is_null($hive)) {
  2758. $implicit=TRUE;
  2759. $hive=$fw->hive();
  2760. }
  2761. if ($this->level<1 || $implicit) {
  2762. if (!$fw->CLI && $mime && !headers_sent() &&
  2763. !preg_grep ('/^Content-Type:/',headers_list()))
  2764. header('Content-Type: '.$mime.'; '.
  2765. 'charset='.$fw->ENCODING);
  2766. if ($fw->ESCAPE && (!$mime ||
  2767. preg_match('/^(text\/html|(application|text)\/(.+\+)?xml)$/i',$mime)))
  2768. $hive=$this->esc($hive);
  2769. if (isset($hive['ALIASES']))
  2770. $hive['ALIASES']=$fw->build($hive['ALIASES']);
  2771. }
  2772. $this->temp=$hive;
  2773. unset($fw,$hive,$implicit,$mime);
  2774. extract($this->temp);
  2775. $this->temp=NULL;
  2776. $this->level++;
  2777. ob_start();
  2778. require($this->file);
  2779. $this->level--;
  2780. return ob_get_clean();
  2781. }
  2782. /**
  2783. * Render template
  2784. * @return string
  2785. * @param $file string
  2786. * @param $mime string
  2787. * @param $hive array
  2788. * @param $ttl int
  2789. **/
  2790. function render($file,$mime='text/html',array $hive=NULL,$ttl=0) {
  2791. $fw=$this->fw;
  2792. $cache=Cache::instance();
  2793. foreach ($fw->split($fw->UI) as $dir) {
  2794. if ($cache->exists($hash=$fw->hash($dir.$file),$data))
  2795. return $data;
  2796. if (is_file($this->file=$fw->fixslashes($dir.$file))) {
  2797. if (isset($_COOKIE[session_name()]) &&
  2798. !headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
  2799. session_start();
  2800. $fw->sync('SESSION');
  2801. $data=$this->sandbox($hive,$mime);
  2802. if (isset($this->trigger['afterrender']))
  2803. foreach($this->trigger['afterrender'] as $func)
  2804. $data=$fw->call($func,[$data, $dir.$file]);
  2805. if ($ttl)
  2806. $cache->set($hash,$data,$ttl);
  2807. return $data;
  2808. }
  2809. }
  2810. user_error(sprintf(Base::E_Open,$file),E_USER_ERROR);
  2811. }
  2812. /**
  2813. * post rendering handler
  2814. * @param $func callback
  2815. */
  2816. function afterrender($func) {
  2817. $this->trigger['afterrender'][]=$func;
  2818. }
  2819. }
  2820. //! Lightweight template engine
  2821. class Preview extends View {
  2822. protected
  2823. //! token filter
  2824. $filter=[
  2825. 'c'=>'$this->c',
  2826. 'esc'=>'$this->esc',
  2827. 'raw'=>'$this->raw',
  2828. 'export'=>'Base::instance()->export',
  2829. 'alias'=>'Base::instance()->alias',
  2830. 'format'=>'Base::instance()->format'
  2831. ];
  2832. protected
  2833. //! newline interpolation
  2834. $interpolation=true;
  2835. /**
  2836. * Enable/disable markup parsing interpolation
  2837. * mainly used for adding appropriate newlines
  2838. * @param $bool bool
  2839. */
  2840. function interpolation($bool) {
  2841. $this->interpolation=$bool;
  2842. }
  2843. /**
  2844. * Return C-locale equivalent of number
  2845. * @return string
  2846. * @param $val int|float
  2847. **/
  2848. function c($val) {
  2849. $locale=setlocale(LC_NUMERIC,0);
  2850. setlocale(LC_NUMERIC,'C');
  2851. $out=(string)(float)$val;
  2852. $locale=setlocale(LC_NUMERIC,$locale);
  2853. return $out;
  2854. }
  2855. /**
  2856. * Convert token to variable
  2857. * @return string
  2858. * @param $str string
  2859. **/
  2860. function token($str) {
  2861. $str=trim(preg_replace('/\{\{(.+?)\}\}/s','\1',$this->fw->compile($str)));
  2862. if (preg_match('/^(.+)(?<!\|)\|((?:\h*\w+(?:\h*[,;]?))+)$/s',
  2863. $str,$parts)) {
  2864. $str=trim($parts[1]);
  2865. foreach ($this->fw->split(trim($parts[2],"\xC2\xA0")) as $func)
  2866. $str=((empty($this->filter[$cmd=$func]) &&
  2867. function_exists($cmd)) ||
  2868. is_string($cmd=$this->filter($func)))?
  2869. $cmd.'('.$str.')':
  2870. 'Base::instance()->'.
  2871. 'call($this->filter(\''.$func.'\'),['.$str.'])';
  2872. }
  2873. return $str;
  2874. }
  2875. /**
  2876. * Register or get (one specific or all) token filters
  2877. * @param string $key
  2878. * @param string|closure $func
  2879. * @return array|closure|string
  2880. */
  2881. function filter($key=NULL,$func=NULL) {
  2882. if (!$key)
  2883. return array_keys($this->filter);
  2884. $key=strtolower($key);
  2885. if (!$func)
  2886. return $this->filter[$key];
  2887. $this->filter[$key]=$func;
  2888. }
  2889. /**
  2890. * Assemble markup
  2891. * @return string
  2892. * @param $node string
  2893. **/
  2894. protected function build($node) {
  2895. return preg_replace_callback(
  2896. '/\{~(.+?)~\}|\{\*(.+?)\*\}|\{\-(.+?)\-\}|'.
  2897. '\{\{(.+?)\}\}((\r?\n)*)/s',
  2898. function($expr) {
  2899. if ($expr[1])
  2900. $str='<?php '.$this->token($expr[1]).' ?>';
  2901. elseif ($expr[2])
  2902. return '';
  2903. elseif ($expr[3])
  2904. $str=$expr[3];
  2905. else {
  2906. $str='<?= ('.trim($this->token($expr[4])).')'.
  2907. ($this->interpolation?
  2908. (!empty($expr[6])?'."'.$expr[6].'"':''):'').' ?>';
  2909. if (isset($expr[5]))
  2910. $str.=$expr[5];
  2911. }
  2912. return $str;
  2913. },
  2914. $node
  2915. );
  2916. }
  2917. /**
  2918. * Render template string
  2919. * @return string
  2920. * @param $node string|array
  2921. * @param $hive array
  2922. * @param $ttl int
  2923. * @param $persist bool
  2924. * @param $escape bool
  2925. **/
  2926. function resolve($node,array $hive=NULL,$ttl=0,$persist=FALSE,$escape=NULL) {
  2927. $fw=$this->fw;
  2928. $cache=Cache::instance();
  2929. if ($escape!==NULL) {
  2930. $esc=$fw->ESCAPE;
  2931. $fw->ESCAPE=$escape;
  2932. }
  2933. if ($ttl || $persist)
  2934. $hash=$fw->hash($fw->serialize($node));
  2935. if ($ttl && $cache->exists($hash,$data))
  2936. return $data;
  2937. if ($persist) {
  2938. if (!is_dir($tmp=$fw->TEMP))
  2939. mkdir($tmp,Base::MODE,TRUE);
  2940. if (!is_file($this->file=($tmp.
  2941. $fw->SEED.'.'.$hash.'.php')))
  2942. $fw->write($this->file,$this->build($node));
  2943. if (isset($_COOKIE[session_name()]) &&
  2944. !headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
  2945. session_start();
  2946. $fw->sync('SESSION');
  2947. $data=$this->sandbox($hive);
  2948. }
  2949. else {
  2950. if (!$hive)
  2951. $hive=$fw->hive();
  2952. if ($fw->ESCAPE)
  2953. $hive=$this->esc($hive);
  2954. extract($hive);
  2955. unset($hive);
  2956. ob_start();
  2957. eval(' ?>'.$this->build($node).'<?php ');
  2958. $data=ob_get_clean();
  2959. }
  2960. if ($ttl)
  2961. $cache->set($hash,$data,$ttl);
  2962. if ($escape!==NULL)
  2963. $fw->ESCAPE=$esc;
  2964. return $data;
  2965. }
  2966. /**
  2967. * Parse template string
  2968. * @return string
  2969. * @param $text string
  2970. **/
  2971. function parse($text) {
  2972. // Remove PHP code and comments
  2973. return preg_replace(
  2974. '/\h*<\?(?!xml)(?:php|\s*=)?.+?\?>\h*|'.
  2975. '\{\*.+?\*\}/is','', $text);
  2976. }
  2977. /**
  2978. * Render template
  2979. * @return string
  2980. * @param $file string
  2981. * @param $mime string
  2982. * @param $hive array
  2983. * @param $ttl int
  2984. **/
  2985. function render($file,$mime='text/html',array $hive=NULL,$ttl=0) {
  2986. $fw=$this->fw;
  2987. $cache=Cache::instance();
  2988. if (!is_dir($tmp=$fw->TEMP))
  2989. mkdir($tmp,Base::MODE,TRUE);
  2990. foreach ($fw->split($fw->UI) as $dir) {
  2991. if ($cache->exists($hash=$fw->hash($dir.$file),$data))
  2992. return $data;
  2993. if (is_file($view=$fw->fixslashes($dir.$file))) {
  2994. if (!is_file($this->file=($tmp.
  2995. $fw->SEED.'.'.$fw->hash($view).'.php')) ||
  2996. filemtime($this->file)<filemtime($view)) {
  2997. $contents=$fw->read($view);
  2998. if (isset($this->trigger['beforerender']))
  2999. foreach ($this->trigger['beforerender'] as $func)
  3000. $contents=$fw->call($func, [$contents, $view]);
  3001. $text=$this->parse($contents);
  3002. $fw->write($this->file,$this->build($text));
  3003. }
  3004. if (isset($_COOKIE[session_name()]) &&
  3005. !headers_sent() && session_status()!=PHP_SESSION_ACTIVE)
  3006. session_start();
  3007. $fw->sync('SESSION');
  3008. $data=$this->sandbox($hive,$mime);
  3009. if(isset($this->trigger['afterrender']))
  3010. foreach ($this->trigger['afterrender'] as $func)
  3011. $data=$fw->call($func, [$data, $view]);
  3012. if ($ttl)
  3013. $cache->set($hash,$data,$ttl);
  3014. return $data;
  3015. }
  3016. }
  3017. user_error(sprintf(Base::E_Open,$file),E_USER_ERROR);
  3018. }
  3019. /**
  3020. * post rendering handler
  3021. * @param $func callback
  3022. */
  3023. function beforerender($func) {
  3024. $this->trigger['beforerender'][]=$func;
  3025. }
  3026. }
  3027. //! ISO language/country codes
  3028. class ISO extends Prefab {
  3029. //@{ ISO 3166-1 country codes
  3030. const
  3031. CC_af='Afghanistan',
  3032. CC_ax='Åland Islands',
  3033. CC_al='Albania',
  3034. CC_dz='Algeria',
  3035. CC_as='American Samoa',
  3036. CC_ad='Andorra',
  3037. CC_ao='Angola',
  3038. CC_ai='Anguilla',
  3039. CC_aq='Antarctica',
  3040. CC_ag='Antigua and Barbuda',
  3041. CC_ar='Argentina',
  3042. CC_am='Armenia',
  3043. CC_aw='Aruba',
  3044. CC_au='Australia',
  3045. CC_at='Austria',
  3046. CC_az='Azerbaijan',
  3047. CC_bs='Bahamas',
  3048. CC_bh='Bahrain',
  3049. CC_bd='Bangladesh',
  3050. CC_bb='Barbados',
  3051. CC_by='Belarus',
  3052. CC_be='Belgium',
  3053. CC_bz='Belize',
  3054. CC_bj='Benin',
  3055. CC_bm='Bermuda',
  3056. CC_bt='Bhutan',
  3057. CC_bo='Bolivia',
  3058. CC_bq='Bonaire, Sint Eustatius and Saba',
  3059. CC_ba='Bosnia and Herzegovina',
  3060. CC_bw='Botswana',
  3061. CC_bv='Bouvet Island',
  3062. CC_br='Brazil',
  3063. CC_io='British Indian Ocean Territory',
  3064. CC_bn='Brunei Darussalam',
  3065. CC_bg='Bulgaria',
  3066. CC_bf='Burkina Faso',
  3067. CC_bi='Burundi',
  3068. CC_kh='Cambodia',
  3069. CC_cm='Cameroon',
  3070. CC_ca='Canada',
  3071. CC_cv='Cape Verde',
  3072. CC_ky='Cayman Islands',
  3073. CC_cf='Central African Republic',
  3074. CC_td='Chad',
  3075. CC_cl='Chile',
  3076. CC_cn='China',
  3077. CC_cx='Christmas Island',
  3078. CC_cc='Cocos (Keeling) Islands',
  3079. CC_co='Colombia',
  3080. CC_km='Comoros',
  3081. CC_cg='Congo',
  3082. CC_cd='Congo, The Democratic Republic of',
  3083. CC_ck='Cook Islands',
  3084. CC_cr='Costa Rica',
  3085. CC_ci='Côte d\'ivoire',
  3086. CC_hr='Croatia',
  3087. CC_cu='Cuba',
  3088. CC_cw='Curaçao',
  3089. CC_cy='Cyprus',
  3090. CC_cz='Czech Republic',
  3091. CC_dk='Denmark',
  3092. CC_dj='Djibouti',
  3093. CC_dm='Dominica',
  3094. CC_do='Dominican Republic',
  3095. CC_ec='Ecuador',
  3096. CC_eg='Egypt',
  3097. CC_sv='El Salvador',
  3098. CC_gq='Equatorial Guinea',
  3099. CC_er='Eritrea',
  3100. CC_ee='Estonia',
  3101. CC_et='Ethiopia',
  3102. CC_fk='Falkland Islands (Malvinas)',
  3103. CC_fo='Faroe Islands',
  3104. CC_fj='Fiji',
  3105. CC_fi='Finland',
  3106. CC_fr='France',
  3107. CC_gf='French Guiana',
  3108. CC_pf='French Polynesia',
  3109. CC_tf='French Southern Territories',
  3110. CC_ga='Gabon',
  3111. CC_gm='Gambia',
  3112. CC_ge='Georgia',
  3113. CC_de='Germany',
  3114. CC_gh='Ghana',
  3115. CC_gi='Gibraltar',
  3116. CC_gr='Greece',
  3117. CC_gl='Greenland',
  3118. CC_gd='Grenada',
  3119. CC_gp='Guadeloupe',
  3120. CC_gu='Guam',
  3121. CC_gt='Guatemala',
  3122. CC_gg='Guernsey',
  3123. CC_gn='Guinea',
  3124. CC_gw='Guinea-Bissau',
  3125. CC_gy='Guyana',
  3126. CC_ht='Haiti',
  3127. CC_hm='Heard Island and McDonald Islands',
  3128. CC_va='Holy See (Vatican City State)',
  3129. CC_hn='Honduras',
  3130. CC_hk='Hong Kong',
  3131. CC_hu='Hungary',
  3132. CC_is='Iceland',
  3133. CC_in='India',
  3134. CC_id='Indonesia',
  3135. CC_ir='Iran, Islamic Republic of',
  3136. CC_iq='Iraq',
  3137. CC_ie='Ireland',
  3138. CC_im='Isle of Man',
  3139. CC_il='Israel',
  3140. CC_it='Italy',
  3141. CC_jm='Jamaica',
  3142. CC_jp='Japan',
  3143. CC_je='Jersey',
  3144. CC_jo='Jordan',
  3145. CC_kz='Kazakhstan',
  3146. CC_ke='Kenya',
  3147. CC_ki='Kiribati',
  3148. CC_kp='Korea, Democratic People\'s Republic of',
  3149. CC_kr='Korea, Republic of',
  3150. CC_kw='Kuwait',
  3151. CC_kg='Kyrgyzstan',
  3152. CC_la='Lao People\'s Democratic Republic',
  3153. CC_lv='Latvia',
  3154. CC_lb='Lebanon',
  3155. CC_ls='Lesotho',
  3156. CC_lr='Liberia',
  3157. CC_ly='Libya',
  3158. CC_li='Liechtenstein',
  3159. CC_lt='Lithuania',
  3160. CC_lu='Luxembourg',
  3161. CC_mo='Macao',
  3162. CC_mk='Macedonia, The Former Yugoslav Republic of',
  3163. CC_mg='Madagascar',
  3164. CC_mw='Malawi',
  3165. CC_my='Malaysia',
  3166. CC_mv='Maldives',
  3167. CC_ml='Mali',
  3168. CC_mt='Malta',
  3169. CC_mh='Marshall Islands',
  3170. CC_mq='Martinique',
  3171. CC_mr='Mauritania',
  3172. CC_mu='Mauritius',
  3173. CC_yt='Mayotte',
  3174. CC_mx='Mexico',
  3175. CC_fm='Micronesia, Federated States of',
  3176. CC_md='Moldova, Republic of',
  3177. CC_mc='Monaco',
  3178. CC_mn='Mongolia',
  3179. CC_me='Montenegro',
  3180. CC_ms='Montserrat',
  3181. CC_ma='Morocco',
  3182. CC_mz='Mozambique',
  3183. CC_mm='Myanmar',
  3184. CC_na='Namibia',
  3185. CC_nr='Nauru',
  3186. CC_np='Nepal',
  3187. CC_nl='Netherlands',
  3188. CC_nc='New Caledonia',
  3189. CC_nz='New Zealand',
  3190. CC_ni='Nicaragua',
  3191. CC_ne='Niger',
  3192. CC_ng='Nigeria',
  3193. CC_nu='Niue',
  3194. CC_nf='Norfolk Island',
  3195. CC_mp='Northern Mariana Islands',
  3196. CC_no='Norway',
  3197. CC_om='Oman',
  3198. CC_pk='Pakistan',
  3199. CC_pw='Palau',
  3200. CC_ps='Palestinian Territory, Occupied',
  3201. CC_pa='Panama',
  3202. CC_pg='Papua New Guinea',
  3203. CC_py='Paraguay',
  3204. CC_pe='Peru',
  3205. CC_ph='Philippines',
  3206. CC_pn='Pitcairn',
  3207. CC_pl='Poland',
  3208. CC_pt='Portugal',
  3209. CC_pr='Puerto Rico',
  3210. CC_qa='Qatar',
  3211. CC_re='Réunion',
  3212. CC_ro='Romania',
  3213. CC_ru='Russian Federation',
  3214. CC_rw='Rwanda',
  3215. CC_bl='Saint Barthélemy',
  3216. CC_sh='Saint Helena, Ascension and Tristan da Cunha',
  3217. CC_kn='Saint Kitts and Nevis',
  3218. CC_lc='Saint Lucia',
  3219. CC_mf='Saint Martin (French Part)',
  3220. CC_pm='Saint Pierre and Miquelon',
  3221. CC_vc='Saint Vincent and The Grenadines',
  3222. CC_ws='Samoa',
  3223. CC_sm='San Marino',
  3224. CC_st='Sao Tome and Principe',
  3225. CC_sa='Saudi Arabia',
  3226. CC_sn='Senegal',
  3227. CC_rs='Serbia',
  3228. CC_sc='Seychelles',
  3229. CC_sl='Sierra Leone',
  3230. CC_sg='Singapore',
  3231. CC_sk='Slovakia',
  3232. CC_sx='Sint Maarten (Dutch Part)',
  3233. CC_si='Slovenia',
  3234. CC_sb='Solomon Islands',
  3235. CC_so='Somalia',
  3236. CC_za='South Africa',
  3237. CC_gs='South Georgia and The South Sandwich Islands',
  3238. CC_ss='South Sudan',
  3239. CC_es='Spain',
  3240. CC_lk='Sri Lanka',
  3241. CC_sd='Sudan',
  3242. CC_sr='Suriname',
  3243. CC_sj='Svalbard and Jan Mayen',
  3244. CC_sz='Swaziland',
  3245. CC_se='Sweden',
  3246. CC_ch='Switzerland',
  3247. CC_sy='Syrian Arab Republic',
  3248. CC_tw='Taiwan, Province of China',
  3249. CC_tj='Tajikistan',
  3250. CC_tz='Tanzania, United Republic of',
  3251. CC_th='Thailand',
  3252. CC_tl='Timor-Leste',
  3253. CC_tg='Togo',
  3254. CC_tk='Tokelau',
  3255. CC_to='Tonga',
  3256. CC_tt='Trinidad and Tobago',
  3257. CC_tn='Tunisia',
  3258. CC_tr='Turkey',
  3259. CC_tm='Turkmenistan',
  3260. CC_tc='Turks and Caicos Islands',
  3261. CC_tv='Tuvalu',
  3262. CC_ug='Uganda',
  3263. CC_ua='Ukraine',
  3264. CC_ae='United Arab Emirates',
  3265. CC_gb='United Kingdom',
  3266. CC_us='United States',
  3267. CC_um='United States Minor Outlying Islands',
  3268. CC_uy='Uruguay',
  3269. CC_uz='Uzbekistan',
  3270. CC_vu='Vanuatu',
  3271. CC_ve='Venezuela',
  3272. CC_vn='Viet Nam',
  3273. CC_vg='Virgin Islands, British',
  3274. CC_vi='Virgin Islands, U.S.',
  3275. CC_wf='Wallis and Futuna',
  3276. CC_eh='Western Sahara',
  3277. CC_ye='Yemen',
  3278. CC_zm='Zambia',
  3279. CC_zw='Zimbabwe';
  3280. //@}
  3281. //@{ ISO 639-1 language codes (Windows-compatibility subset)
  3282. const
  3283. LC_af='Afrikaans',
  3284. LC_am='Amharic',
  3285. LC_ar='Arabic',
  3286. LC_as='Assamese',
  3287. LC_ba='Bashkir',
  3288. LC_be='Belarusian',
  3289. LC_bg='Bulgarian',
  3290. LC_bn='Bengali',
  3291. LC_bo='Tibetan',
  3292. LC_br='Breton',
  3293. LC_ca='Catalan',
  3294. LC_co='Corsican',
  3295. LC_cs='Czech',
  3296. LC_cy='Welsh',
  3297. LC_da='Danish',
  3298. LC_de='German',
  3299. LC_dv='Divehi',
  3300. LC_el='Greek',
  3301. LC_en='English',
  3302. LC_es='Spanish',
  3303. LC_et='Estonian',
  3304. LC_eu='Basque',
  3305. LC_fa='Persian',
  3306. LC_fi='Finnish',
  3307. LC_fo='Faroese',
  3308. LC_fr='French',
  3309. LC_gd='Scottish Gaelic',
  3310. LC_gl='Galician',
  3311. LC_gu='Gujarati',
  3312. LC_he='Hebrew',
  3313. LC_hi='Hindi',
  3314. LC_hr='Croatian',
  3315. LC_hu='Hungarian',
  3316. LC_hy='Armenian',
  3317. LC_id='Indonesian',
  3318. LC_ig='Igbo',
  3319. LC_is='Icelandic',
  3320. LC_it='Italian',
  3321. LC_ja='Japanese',
  3322. LC_ka='Georgian',
  3323. LC_kk='Kazakh',
  3324. LC_km='Khmer',
  3325. LC_kn='Kannada',
  3326. LC_ko='Korean',
  3327. LC_lb='Luxembourgish',
  3328. LC_lo='Lao',
  3329. LC_lt='Lithuanian',
  3330. LC_lv='Latvian',
  3331. LC_mi='Maori',
  3332. LC_ml='Malayalam',
  3333. LC_mr='Marathi',
  3334. LC_ms='Malay',
  3335. LC_mt='Maltese',
  3336. LC_ne='Nepali',
  3337. LC_nl='Dutch',
  3338. LC_no='Norwegian',
  3339. LC_oc='Occitan',
  3340. LC_or='Oriya',
  3341. LC_pl='Polish',
  3342. LC_ps='Pashto',
  3343. LC_pt='Portuguese',
  3344. LC_qu='Quechua',
  3345. LC_ro='Romanian',
  3346. LC_ru='Russian',
  3347. LC_rw='Kinyarwanda',
  3348. LC_sa='Sanskrit',
  3349. LC_si='Sinhala',
  3350. LC_sk='Slovak',
  3351. LC_sl='Slovenian',
  3352. LC_sq='Albanian',
  3353. LC_sv='Swedish',
  3354. LC_ta='Tamil',
  3355. LC_te='Telugu',
  3356. LC_th='Thai',
  3357. LC_tk='Turkmen',
  3358. LC_tr='Turkish',
  3359. LC_tt='Tatar',
  3360. LC_uk='Ukrainian',
  3361. LC_ur='Urdu',
  3362. LC_vi='Vietnamese',
  3363. LC_wo='Wolof',
  3364. LC_yo='Yoruba',
  3365. LC_zh='Chinese';
  3366. //@}
  3367. /**
  3368. * Return list of languages indexed by ISO 639-1 language code
  3369. * @return array
  3370. **/
  3371. function languages() {
  3372. return \Base::instance()->constants($this,'LC_');
  3373. }
  3374. /**
  3375. * Return list of countries indexed by ISO 3166-1 country code
  3376. * @return array
  3377. **/
  3378. function countries() {
  3379. return \Base::instance()->constants($this,'CC_');
  3380. }
  3381. }
  3382. //! Container for singular object instances
  3383. final class Registry {
  3384. private static
  3385. //! Object catalog
  3386. $table;
  3387. /**
  3388. * Return TRUE if object exists in catalog
  3389. * @return bool
  3390. * @param $key string
  3391. **/
  3392. static function exists($key) {
  3393. return isset(self::$table[$key]);
  3394. }
  3395. /**
  3396. * Add object to catalog
  3397. * @return object
  3398. * @param $key string
  3399. * @param $obj object
  3400. **/
  3401. static function set($key,$obj) {
  3402. return self::$table[$key]=$obj;
  3403. }
  3404. /**
  3405. * Retrieve object from catalog
  3406. * @return object
  3407. * @param $key string
  3408. **/
  3409. static function get($key) {
  3410. return self::$table[$key];
  3411. }
  3412. /**
  3413. * Delete object from catalog
  3414. * @param $key string
  3415. **/
  3416. static function clear($key) {
  3417. self::$table[$key]=NULL;
  3418. unset(self::$table[$key]);
  3419. }
  3420. //! Prohibit cloning
  3421. private function __clone() {
  3422. }
  3423. //! Prohibit instantiation
  3424. private function __construct() {
  3425. }
  3426. }
  3427. return Base::instance();