PageRenderTime 29ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/com/controller.php

http://github.com/unirgy/buckyball
PHP | 2261 lines | 1342 code | 186 blank | 733 comment | 182 complexity | 95f55a4bc3a005edd6f694998283f981 MD5 | raw file
Possible License(s): LGPL-2.1, BSD-3-Clause
  1. <?php
  2. /**
  3. * Copyright 2011 Unirgy LLC
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. *
  17. * @package BuckyBall
  18. * @link http://github.com/unirgy/buckyball
  19. * @author Boris Gurvich <boris@unirgy.com>
  20. * @copyright (c) 2010-2012 Boris Gurvich
  21. * @license http://www.apache.org/licenses/LICENSE-2.0.html
  22. */
  23. /**
  24. * Facility to handle request input
  25. */
  26. class BRequest extends BClass
  27. {
  28. /**
  29. * Route parameters
  30. *
  31. * Taken from route, ex:
  32. * Route: /part1/:param1/part2/:param2
  33. * Request: /part1/test1/param2/test2
  34. * $_params: array('param1'=>'test1', 'param2'=>'test2')
  35. *
  36. * @var array
  37. */
  38. protected $_params = array();
  39. /**
  40. * Shortcut to help with IDE autocompletion
  41. *
  42. * @return BRequest
  43. */
  44. public static function i($new=false, array $args=array())
  45. {
  46. return BClassRegistry::i()->instance(__CLASS__, $args, !$new);
  47. }
  48. /**
  49. * On first invokation strip magic quotes in case magic_quotes_gpc = on
  50. *
  51. * @return BRequest
  52. */
  53. public function __construct()
  54. {
  55. $this->stripMagicQuotes();
  56. if (!empty($_SERVER['ORIG_SCRIPT_NAME'])) {
  57. $_SERVER['ORIG_SCRIPT_NAME'] = str_replace('/index.php/index.php', '/index.php', $_SERVER['ORIG_SCRIPT_NAME']);
  58. }
  59. if (!empty($_SERVER['ORIG_SCRIPT_FILENAME'])) {
  60. $_SERVER['ORIG_SCRIPT_FILENAME'] = str_replace('/index.php/index.php', '/index.php', $_SERVER['ORIG_SCRIPT_FILENAME']);
  61. }
  62. }
  63. /**
  64. * Client remote IP
  65. *
  66. * @return string
  67. */
  68. public static function ip()
  69. {
  70. return !empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
  71. }
  72. /**
  73. * Server local IP
  74. *
  75. * @return string
  76. */
  77. public static function serverIp()
  78. {
  79. return !empty($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : null;
  80. }
  81. /**
  82. * Server host name
  83. *
  84. * @return string
  85. */
  86. public static function serverName()
  87. {
  88. return !empty($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null;
  89. }
  90. /**
  91. * Host name from request headers
  92. *
  93. * @return string
  94. */
  95. public static function httpHost($includePort = true)
  96. {
  97. if (empty($_SERVER['HTTP_HOST'])) {
  98. return null;
  99. }
  100. if ($includePort) {
  101. return $_SERVER['HTTP_HOST'];
  102. }
  103. $a = explode(':', $_SERVER['HTTP_HOST']);
  104. return $a[0];
  105. }
  106. /**
  107. * Port from request headers
  108. *
  109. * @return string
  110. */
  111. public static function httpPort()
  112. {
  113. return !empty($_SERVER['HTTP_PORT']) ? $_SERVER['HTTP_PORT'] : null;
  114. }
  115. /**
  116. * Origin host name from request headers
  117. *
  118. * @return string
  119. */
  120. public static function httpOrigin()
  121. {
  122. return !empty($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : null;
  123. }
  124. /**
  125. * Whether request is SSL
  126. *
  127. * @return bool
  128. */
  129. public static function https()
  130. {
  131. return !empty($_SERVER['HTTPS']);
  132. }
  133. /**
  134. * Server protocol (HTTP/1.0 or HTTP/1.1)
  135. *
  136. * @return string
  137. */
  138. public static function serverProtocol()
  139. {
  140. $protocol = "HTTP/1.0";
  141. if(isset($_SERVER['SERVER_PROTOCOL']) && stripos($_SERVER['SERVER_PROTOCOL'],"HTTP") >= 0){
  142. $protocol = $_SERVER['SERVER_PROTOCOL'];
  143. }
  144. return $protocol;
  145. }
  146. public static function scheme()
  147. {
  148. return static::https() ? 'https' : 'http';
  149. }
  150. /**
  151. * Retrive language based on HTTP_ACCEPT_LANGUAGE
  152. * @return string
  153. */
  154. static public function language()
  155. {
  156. $langs = array();
  157. if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
  158. // break up string into pieces (languages and q factors)
  159. preg_match_all('/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $lang_parse);
  160. if (count($lang_parse[1])) {
  161. // create a list like "en" => 0.8
  162. $langs = array_combine($lang_parse[1], $lang_parse[4]);
  163. // set default to 1 for any without q factor
  164. foreach ($langs as $lang => $val) {
  165. if ($val === '') $langs[$lang] = 1;
  166. }
  167. // sort list based on value
  168. arsort($langs, SORT_NUMERIC);
  169. }
  170. }
  171. //if no language detected return false
  172. if (empty($langs)) {
  173. return false;
  174. }
  175. list($toplang) = each($langs);
  176. //return en, de, es, it.... first two characters of language code
  177. return substr($toplang, 0, 2);
  178. }
  179. /**
  180. * Whether request is AJAX
  181. *
  182. * @return bool
  183. */
  184. public static function xhr()
  185. {
  186. return !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH']=='XMLHttpRequest';
  187. }
  188. /**
  189. * Request method:
  190. *
  191. * @return string GET|POST|HEAD|PUT|DELETE
  192. */
  193. public static function method()
  194. {
  195. return !empty($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
  196. }
  197. /**
  198. * Web server document root dir
  199. *
  200. * @return string
  201. */
  202. public static function docRoot()
  203. {
  204. return !empty($_SERVER['DOCUMENT_ROOT']) ? str_replace('\\', '/', $_SERVER['DOCUMENT_ROOT']) : null;
  205. }
  206. /**
  207. * Entry point script web path
  208. *
  209. * @return string
  210. */
  211. public static function scriptName()
  212. {
  213. return !empty($_SERVER['SCRIPT_NAME']) ? str_replace('\\', '/', $_SERVER['SCRIPT_NAME']) :
  214. (!empty($_SERVER['ORIG_SCRIPT_NAME']) ? str_replace('\\', '/', $_SERVER['ORIG_SCRIPT_NAME']) : null);
  215. }
  216. /**
  217. * Entry point script file name
  218. *
  219. * @return string
  220. */
  221. public static function scriptFilename()
  222. {
  223. return !empty($_SERVER['SCRIPT_FILENAME']) ? str_replace('\\', '/', $_SERVER['SCRIPT_FILENAME']) :
  224. (!empty($_SERVER['ORIG_SCRIPT_FILENAME']) ? str_replace('\\', '/', $_SERVER['ORIG_SCRIPT_FILENAME']) : null);
  225. }
  226. /**
  227. * Entry point directory name
  228. *
  229. * @return string
  230. */
  231. public static function scriptDir()
  232. {
  233. return ($script = static::scriptFilename()) ? dirname($script) : null;
  234. }
  235. /**
  236. * Web root path for current application
  237. *
  238. * If request is /folder1/folder2/index.php, return /folder1/folder2/
  239. *
  240. * @param $parent if required a parent of current web root, specify depth
  241. * @return string
  242. */
  243. public static function webRoot($parentDepth=null)
  244. {
  245. $scriptName = static::scriptName();
  246. if (empty($scriptName)) {
  247. return null;
  248. }
  249. $root = rtrim(str_replace(array('//', '\\'), array('/', '/'), dirname($scriptName)), '/');
  250. if ($parentDepth) {
  251. $arr = explode('/', rtrim($root, '/'));
  252. $len = sizeof($arr)-$parentDepth;
  253. $root = $len>1 ? join('/', array_slice($arr, 0, $len)) : '/';
  254. }
  255. return $root ? $root : '/';
  256. }
  257. /**
  258. * Full base URL, including scheme and domain name
  259. *
  260. * @todo optional omit http(s):
  261. * @param null|boolean $forceSecure - if not null, force scheme
  262. * @param boolean $includeQuery - add origin query string
  263. * @return string
  264. */
  265. public static function baseUrl($forceSecure=null, $includeQuery=false)
  266. {
  267. if (is_null($forceSecure)) {
  268. $scheme = static::https() ? 'https:' : '';
  269. } else {
  270. $scheme = $forceSecure ? 'https:' : '';
  271. }
  272. $url = $scheme.'//'.static::serverName().static::webRoot();
  273. if ($includeQuery && ($query = static::rawGet())) {
  274. $url .= '?'.$query;
  275. }
  276. return $url;
  277. }
  278. /**
  279. * Full request path, one part or slice of path
  280. *
  281. * @param int $offset
  282. * @param int $length
  283. * @return string
  284. */
  285. public static function path($offset, $length=null)
  286. {
  287. $pathInfo = !empty($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] :
  288. (!empty($_SERVER['ORIG_PATH_INFO']) ? $_SERVER['ORIG_PATH_INFO'] : null);
  289. if (empty($pathInfo)) {
  290. return null;
  291. }
  292. $path = explode('/', ltrim($pathInfo, '/'));
  293. if (is_null($length)) {
  294. return isset($path[$offset]) ? $path[$offset] : null;
  295. }
  296. return join('/', array_slice($path, $offset, true===$length ? null : $length));
  297. }
  298. /**
  299. * Raw path string
  300. *
  301. * @return string
  302. */
  303. public static function rawPath()
  304. {
  305. #echo "<pre>"; print_r($_SERVER); exit;
  306. return !empty($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] :
  307. (!empty($_SERVER['ORIG_PATH_INFO']) ? $_SERVER['ORIG_PATH_INFO'] : '/');
  308. /*
  309. (!empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] :
  310. (!empty($_SERVER['SERVER_URL']) ? $_SERVER['SERVER_URL'] : '/')
  311. )
  312. );*/
  313. }
  314. /**
  315. * PATH_TRANSLATED
  316. *
  317. */
  318. public static function pathTranslated()
  319. {
  320. return !empty($_SERVER['PATH_TRANSLATED']) ? $_SERVER['PATH_TRANSLATED'] :
  321. (!empty($_SERVER['ORIG_PATH_TRANSLATED']) ? $_SERVER['ORIG_PATH_TRANSLATED'] : '/');
  322. }
  323. /**
  324. * Request query variables
  325. *
  326. * @param string $key
  327. * @return array|string|null
  328. */
  329. public static function get($key=null)
  330. {
  331. return is_null($key) ? $_GET : (isset($_GET[$key]) ? $_GET[$key] : null);
  332. }
  333. public static function headers($key=null)
  334. {
  335. $key = strtoupper($key);
  336. return is_null($key) ? $_SERVER : (isset($_SERVER[$key]) ? $_SERVER[$key] : null);
  337. }
  338. /**
  339. * Request query as string
  340. *
  341. * @return string
  342. */
  343. public static function rawGet()
  344. {
  345. return !empty($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
  346. }
  347. /**
  348. * Request POST variables
  349. *
  350. * @param string|null $key
  351. * @return array|string|null
  352. */
  353. public static function post($key=null)
  354. {
  355. return is_null($key) ? $_POST : (isset($_POST[$key]) ? $_POST[$key] : null);
  356. }
  357. /**
  358. * Request raw POST text
  359. *
  360. * @param bool $json Receive request as JSON
  361. * @param bool $asObject Return as object vs array
  362. * @return object|array|string
  363. */
  364. public static function rawPost()
  365. {
  366. $post = file_get_contents('php://input');
  367. return $post;
  368. }
  369. /**
  370. * Request array/object from JSON API call
  371. *
  372. * @param boolean $asObject
  373. * @return mixed
  374. */
  375. public static function json($asObject=false)
  376. {
  377. return BUtil::fromJson(static::rawPost(), $asObject);
  378. }
  379. /**
  380. * Request variable (GET|POST|COOKIE)
  381. *
  382. * @param string|null $key
  383. * @return array|string|null
  384. */
  385. public static function request($key=null)
  386. {
  387. return is_null($key) ? $_REQUEST : (isset($_REQUEST[$key]) ? $_REQUEST[$key] : null);
  388. }
  389. /**
  390. * Set or retrieve cookie value
  391. *
  392. * @param string $name Cookie name
  393. * @param string $value Cookie value to be set
  394. * @param int $lifespan Optional lifespan, default from config
  395. * @param string $path Optional cookie path, default from config
  396. * @param string $domain Optional cookie domain, default from config
  397. */
  398. public static function cookie($name, $value=null, $lifespan=null, $path=null, $domain=null)
  399. {
  400. if (is_null($value)) {
  401. return isset($_COOKIE[$name]) ? $_COOKIE[$name] : null;
  402. }
  403. if (false===$value) {
  404. return static::cookie($name, '', -1000);
  405. }
  406. $config = BConfig::i()->get('cookie');
  407. $lifespan = !is_null($lifespan) ? $lifespan : $config['timeout'];
  408. $path = !is_null($path) ? $path : (!empty($config['path']) ? $config['path'] : static::webRoot());
  409. $domain = !is_null($domain) ? $domain : (!empty($config['domain']) ? $config['domain'] : static::httpHost(false));
  410. setcookie($name, $value, time()+$lifespan, $path, $domain);
  411. }
  412. /**
  413. * Get request referrer
  414. *
  415. * @see http://en.wikipedia.org/wiki/HTTP_referrer#Origin_of_the_term_referer
  416. * @param string $default default value to use in case there is no referrer available
  417. * @return string|null
  418. */
  419. public static function referrer($default=null)
  420. {
  421. return !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : $default;
  422. }
  423. public static function receiveFiles($source, $targetDir, $typesRegex=null)
  424. {
  425. if (is_string($source)) {
  426. if (!empty($_FILES[$source])) {
  427. $source = $_FILES[$source];
  428. } else {
  429. //TODO: missing enctype="multipart/form-data" ?
  430. throw new BException('Missing enctype="multipart/form-data"?');
  431. }
  432. }
  433. if (empty($source)) {
  434. return;
  435. }
  436. $result = array();
  437. $uploadErrors = array(
  438. UPLOAD_ERR_OK => "No errors.",
  439. UPLOAD_ERR_INI_SIZE => "Larger than upload_max_filesize.",
  440. UPLOAD_ERR_FORM_SIZE => "Larger than form MAX_FILE_SIZE.",
  441. UPLOAD_ERR_PARTIAL => "Partial upload.",
  442. UPLOAD_ERR_NO_FILE => "No file.",
  443. UPLOAD_ERR_NO_TMP_DIR => "No temporary directory.",
  444. UPLOAD_ERR_CANT_WRITE => "Can't write to disk.",
  445. UPLOAD_ERR_EXTENSION => "File upload stopped by extension."
  446. );
  447. if (is_array($source['error'])) {
  448. foreach ($source['error'] as $key=>$error) {
  449. if ($error==UPLOAD_ERR_OK) {
  450. $tmpName = $source['tmp_name'][$key];
  451. $name = $source['name'][$key];
  452. $type = $source['type'][$key];
  453. if (!is_null($typesRegex) && !preg_match('#'.$typesRegex.'#i', $type)) {
  454. $result[$key] = array('error'=>'invalid_type', 'tp'=>1, 'type'=>$type, 'name'=>$name);
  455. continue;
  456. }
  457. BUtil::ensureDir($targetDir);
  458. move_uploaded_file($tmpName, $targetDir.'/'.$name);
  459. $result[$key] = array('name'=>$name, 'tp'=>2, 'type'=>$type, 'target'=>$targetDir.'/'.$name);
  460. } else {
  461. $message = !empty($uploadErrors[$error]) ? $uploadErrors[$error] : null;
  462. $result[$key] = array('error'=>$error, 'message' => $message, 'tp'=>3);
  463. }
  464. }
  465. } else {
  466. $error = $source['error'];
  467. if ($error==UPLOAD_ERR_OK) {
  468. $tmpName = $source['tmp_name'];
  469. $name = $source['name'];
  470. $type = $source['type'];
  471. if (!is_null($typesRegex) && !preg_match('#'.$typesRegex.'#i', $type)) {
  472. $result[] = array('error'=>'invalid_type', 'tp'=>4, 'type'=>$type, 'pattern'=>$typesRegex, 'source'=>$source, 'name'=>$name);
  473. } else {
  474. BUtil::ensureDir($targetDir);
  475. move_uploaded_file($tmpName, $targetDir.'/'.$name);
  476. $result[] = array('name'=>$name, 'type'=>$type, 'target'=>$targetDir.'/'.$name);
  477. }
  478. } else {
  479. $message = !empty($uploadErrors[$error]) ? $uploadErrors[$error] : null;
  480. $result[] = array('error'=>$error, 'message' => $message, 'tp'=>5);
  481. }
  482. }
  483. return $result;
  484. }
  485. /**
  486. * Check whether the request can be CSRF attack
  487. *
  488. * Uses HTTP_REFERER header to compare with current host and path.
  489. * By default only POST, DELETE, PUT requests are protected
  490. * Only these methods should be used for data manipulation.
  491. *
  492. * The following specific cases will return csrf true:
  493. * - posting from different host or web root path
  494. * - posting from https to http
  495. *
  496. * @see http://en.wikipedia.org/wiki/Cross-site_request_forgery
  497. *
  498. * @param array $methods Methods to check for CSRF attack
  499. * @return boolean
  500. */
  501. public static function csrf()
  502. {
  503. $c = BConfig::i();
  504. $m = $c->get('web/csrf_http_methods');
  505. $httpMethods = $m ? (is_string($m) ? explode(',', $m) : $m) : array('POST','PUT','DELETE');
  506. if (is_array($httpMethods) && !in_array(static::method(), $httpMethods)) {
  507. return false; // not one of checked methods, pass
  508. }
  509. $whitelist = $c->get('web/csrf_path_whitelist');
  510. if ($whitelist) {
  511. $path = static::rawPath();
  512. foreach ((array)$whitelist as $pattern) {
  513. if (preg_match($pattern, $path)) {
  514. return false;
  515. }
  516. }
  517. }
  518. $m = $c->get('web/csrf_check_method');
  519. $method = $m ? $m : 'referrer';
  520. switch ($method) {
  521. case 'referrer':
  522. if (!($ref = static::referrer())) {
  523. return true; // no referrer sent, high prob. csrf
  524. }
  525. $p = parse_url($ref);
  526. $p['path'] = preg_replace('#/+#', '/', $p['path']); // ignore duplicate slashes
  527. $webRoot = $c->get('web/base_src');
  528. if (!$webRoot) $webRoot = static::webRoot();
  529. if ($p['host']!==static::httpHost(false) || $webRoot && strpos($p['path'], $webRoot)!==0) {
  530. return true; // referrer host or doc root path do not match, high prob. csrf
  531. }
  532. return false; // not csrf
  533. case 'token':
  534. if (!empty($_SERVER['HTTP_X_CSRF_TOKEN'])) {
  535. $receivedToken = $_SERVER['HTTP_X_CSRF_TOKEN'];
  536. } elseif (!empty($_POST['X-CSRF-TOKEN'])) {
  537. $receivedToken = $_POST['X-CSRF-TOKEN'];
  538. }
  539. return empty($receivedToken) || $receivedToken !== BSession::i()->csrfToken();
  540. default:
  541. throw new BException('Invalid CSRF check method: '.$method);
  542. }
  543. }
  544. /**
  545. * Verify that HTTP_HOST or HTTP_ORIGIN
  546. *
  547. * @param string $method (HOST|ORIGIN|OR|AND)
  548. * @param string $explicitHost
  549. * @return boolean
  550. */
  551. public static function verifyOriginHostIp($method='OR', $host=null)
  552. {
  553. $ip = static::ip();
  554. if (!$host) {
  555. $host = static::httpHost(false);
  556. }
  557. $origin = static::httpOrigin();
  558. $hostIPs = gethostbynamel($host);
  559. $hostMatches = $host && $method!='ORIGIN' ? in_array($ip, (array)$hostIPs) : false;
  560. $originIPs = gethostbynamel($origin);
  561. $originMatches = $origin && $method!='HOST' ? in_array($ip, (array)$originIPs) : false;
  562. switch ($method) {
  563. case 'HOST': return $hostMatches;
  564. case 'ORIGIN': return $originMatches;
  565. case 'AND': return $hostMatches && $originMatches;
  566. case 'OR': return $hostMatches || $originMatches;
  567. }
  568. return false;
  569. }
  570. /**
  571. * Get current request URL
  572. *
  573. * @return string
  574. */
  575. public static function currentUrl()
  576. {
  577. $webroot = rtrim(static::webRoot(), '/');
  578. $scheme = static::scheme();
  579. $port = static::httpPort();
  580. $url = $scheme.'://'.static::httpHost();
  581. if (!BConfig::i()->get('web/hide_script_name')) {
  582. $url = rtrim($url, '/') . '/' . ltrim(str_replace('//', '/', static::scriptName()), '/');
  583. } else {
  584. $url = rtrim($url, '/') . '/' . ltrim(str_replace('//', '/', $webroot), '/');;
  585. }
  586. $url .= static::rawPath().(($q = static::rawGet()) ? '?'.$q : '');
  587. return $url;
  588. }
  589. /**
  590. * Initialize route parameters
  591. *
  592. * @param array $params
  593. */
  594. public function initParams(array $params)
  595. {
  596. $this->_params = $params;
  597. return $this;
  598. }
  599. /**
  600. * Return route parameter by name or all parameters as array
  601. *
  602. * @param string $key
  603. * @param boolean $fallbackToGet
  604. * @return array|string|null
  605. */
  606. public function param($key=null, $fallbackToGet=false)
  607. {
  608. if (is_null($key)) {
  609. return $this->_params;
  610. } elseif (isset($this->_params[$key]) && ''!==$this->_params[$key]) {
  611. return $this->_params[$key];
  612. } elseif ($fallbackToGet && !empty($_GET[$key])) {
  613. return $_GET[$key];
  614. } else {
  615. return null;
  616. }
  617. }
  618. /**
  619. * Alias for legacy code
  620. *
  621. * @deprecated
  622. * @param mixed $key
  623. * @param mixed $fallbackToGet
  624. */
  625. public function params($key=null, $fallbackToGet=false)
  626. {
  627. return $this->param($key, $fallbackToGet);
  628. }
  629. /**
  630. * Sanitize input and assign default values
  631. *
  632. * Syntax: BRequest::i()->sanitize($post, array(
  633. * 'var1' => 'alnum', // return only alphanumeric components, default null
  634. * 'var2' => array('trim|ucwords', 'default'), // trim and capitalize, default 'default'
  635. * 'var3' => array('regex:/[^0-9.]/', '0'), // remove anything not number or .
  636. * ));
  637. *
  638. * @todo replace with filter_var_array
  639. *
  640. * @param array|object $data Array to be sanitized
  641. * @param array $config Configuration for sanitizing
  642. * @param bool $trim Whether to return only variables specified in config
  643. * @return array Sanitized result
  644. */
  645. public static function sanitize($data, $config, $trim=true)
  646. {
  647. $data = (array)$data;
  648. if ($trim) {
  649. $data = array_intersect_key($data, $config);
  650. }
  651. foreach ($data as $k=>&$v) {
  652. $filter = is_array($config[$k]) ? $config[$k][0] : $config[$k];
  653. $v = static::sanitizeOne($v, $filter);
  654. }
  655. unset($v);
  656. foreach ($config as $k=>$c) {
  657. if (!isset($data[$k])) {
  658. $data[$k] = is_array($c) && isset($c[1]) ? $c[1] : null;
  659. }
  660. }
  661. return $data;
  662. }
  663. /**
  664. * Sanitize one variable based on specified filter(s)
  665. *
  666. * Filters:
  667. * - int
  668. * - positive
  669. * - float
  670. * - trim
  671. * - nohtml
  672. * - plain
  673. * - upper
  674. * - lower
  675. * - ucwords
  676. * - ucfirst
  677. * - urle
  678. * - urld
  679. * - alnum
  680. * - regex
  681. * - date
  682. * - datetime
  683. * - gmdate
  684. * - gmdatetime
  685. *
  686. * @param string $v Value to be sanitized
  687. * @param array|string $filter Filters as array or string separated by |
  688. * @return string Sanitized value
  689. */
  690. public static function sanitizeOne($v, $filter)
  691. {
  692. if (is_array($v)) {
  693. foreach ($v as $k=>&$v1) {
  694. $v1 = static::sanitizeOne($v1, $filter);
  695. }
  696. unset($v1);
  697. return $v;
  698. }
  699. if (!is_array($filter)) {
  700. $filter = explode('|', $filter);
  701. }
  702. foreach ($filter as $f) {
  703. if (strpos($f, ':')) {
  704. list($f, $p) = explode(':', $f, 2);
  705. } else {
  706. $p = null;
  707. }
  708. switch ($f) {
  709. case 'int': $v = (int)$v; break;
  710. case 'positive': $v = $v>0 ? $v : null; break;
  711. case 'float': $v = (float)$v; break;
  712. case 'trim': $v = trim($v); break;
  713. case 'nohtml': $v = htmlentities($v, ENT_QUOTES); break;
  714. case 'plain': $v = htmlentities($v, ENT_NOQUOTES); break;
  715. case 'upper': $v = strtoupper($v); break;
  716. case 'lower': $v = strtolower($v); break;
  717. case 'ucwords': $v = ucwords($v); break;
  718. case 'ucfirst': $v = ucfirst($v); break;
  719. case 'urle': $v = urlencode($v); break;
  720. case 'urld': $v = urldecode($v); break;
  721. case 'alnum': $p = !empty($p)?$p:'_'; $v = preg_replace('#[^a-z0-9'.$p.']#i', '', $v); break;
  722. case 'regex': case 'regexp': $v = preg_replace($p, '', $v); break;
  723. case 'date': $v = date('Y-m-d', strtotime($v)); break;
  724. case 'datetime': $v = date('Y-m-d H:i:s', strtotime($v)); break;
  725. case 'gmdate': $v = gmdate('Y-m-d', strtotime($v)); break;
  726. case 'gmdatetime': $v = gmdate('Y-m-d H:i:s', strtotime($v)); break;
  727. }
  728. }
  729. return $v;
  730. }
  731. /**
  732. * String magic quotes in case magic_quotes_gpc = on
  733. *
  734. * @return BRequest
  735. */
  736. public static function stripMagicQuotes()
  737. {
  738. static $alreadyRan = false;
  739. if (get_magic_quotes_gpc() && !$alreadyRan) {
  740. $process = array(&$_GET, &$_POST, &$_COOKIE, &$_REQUEST);
  741. while (list($key, $val) = each($process)) {
  742. foreach ($val as $k => $v) {
  743. unset($process[$key][$k]);
  744. if (is_array($v)) {
  745. $process[$key][stripslashes($k)] = $v;
  746. $process[] = &$process[$key][stripslashes($k)];
  747. } else {
  748. $process[$key][stripslashes($k)] = stripslashes($v);
  749. }
  750. }
  751. }
  752. unset($process);
  753. $alreadyRan = true;
  754. }
  755. }
  756. public static function modRewriteEnabled()
  757. {
  758. if (function_exists('apache_get_modules')) {
  759. $modules = apache_get_modules();
  760. $modRewrite = in_array('mod_rewrite', $modules);
  761. } else {
  762. $modRewrite = strtolower(getenv('HTTP_MOD_REWRITE'))=='on' ? true : false;
  763. }
  764. return $modRewrite;
  765. }
  766. }
  767. /**
  768. * Facility to handle response to client
  769. */
  770. class BResponse extends BClass
  771. {
  772. protected static $_httpStatuses = array(
  773. 100 => 'Continue',
  774. 101 => 'Switching Protocols',
  775. 200 => 'OK',
  776. 201 => 'Created',
  777. 202 => 'Accepted',
  778. 203 => 'Non-Authorative Information',
  779. 204 => 'No Content',
  780. 205 => 'Reset Content',
  781. 206 => 'Partial Content',
  782. 300 => 'Multiple Choices',
  783. 301 => 'Moved Permanently',
  784. 302 => 'Found',
  785. 303 => 'See Other',
  786. 304 => 'Not Modified',
  787. 307 => 'Temporary Redirect',
  788. 400 => 'Bad Request',
  789. 401 => 'Unauthorized',
  790. 402 => 'Payment Required',
  791. 403 => 'Forbidden',
  792. 404 => 'Not Found',
  793. 405 => 'Method Not Allowed',
  794. 406 => 'Not Acceptable',
  795. 407 => 'Proxy Authentication Required',
  796. 408 => 'Request Time-out',
  797. 409 => 'Conflict',
  798. 410 => 'Gone',
  799. 411 => 'Length Required',
  800. 412 => 'Precondition Failed',
  801. 413 => 'Request Entity Too Large',
  802. 414 => 'Request-URI Too Large',
  803. 415 => 'Unsupported Media Type',
  804. 416 => 'Requested Range Not Satisfiable',
  805. 417 => 'Expectation Failed',
  806. 500 => 'Internal Server Error',
  807. 501 => 'Not Implemented',
  808. 502 => 'Bad Gateway',
  809. 503 => 'Service Unavailable',
  810. 504 => 'Gateway Time-out',
  811. 505 => 'HTTP Version Not Supported',
  812. );
  813. /**
  814. * Response content MIME type
  815. *
  816. * @var string
  817. */
  818. protected $_contentType = 'text/html';
  819. protected $_charset = 'UTF-8';
  820. protected $_contentPrefix;
  821. protected $_contentSuffix;
  822. /**
  823. * Content to be returned to client
  824. *
  825. * @var mixed
  826. */
  827. protected $_content;
  828. /**
  829. * Shortcut to help with IDE autocompletion
  830. *
  831. * @return BResponse
  832. */
  833. public static function i($new=false, array $args=array())
  834. {
  835. return BClassRegistry::i()->instance(__CLASS__, $args, !$new);
  836. }
  837. /**
  838. * Escape HTML
  839. *
  840. * @param string $str
  841. * @return string
  842. */
  843. public static function q($str)
  844. {
  845. if (is_null($str)) {
  846. return '';
  847. }
  848. if (!is_scalar($str)) {
  849. var_dump($str);
  850. return ' ** ERROR ** ';
  851. }
  852. return htmlspecialchars($str);
  853. }
  854. /**
  855. * Alias for BRequest::i()->cookie()
  856. *
  857. * @param string $name
  858. * @param string $value
  859. * @param int $lifespan
  860. * @param string $path
  861. * @param string $domain
  862. * @return BResponse
  863. */
  864. public function cookie($name, $value=null, $lifespan=null, $path=null, $domain=null)
  865. {
  866. BRequest::cookie($name, $value, $lifespan, $path, $domain);
  867. return $this;
  868. }
  869. /**
  870. * Set response content
  871. *
  872. * @param mixed $content
  873. * @return BResponse
  874. */
  875. public function set($content)
  876. {
  877. $this->_content = $content;
  878. return $this;
  879. }
  880. /**
  881. * Add content to response
  882. *
  883. * @param mixed $content
  884. * @return BResponse
  885. */
  886. public function add($content)
  887. {
  888. $this->_content = (array)$this->_content+(array)$content;
  889. return $this;
  890. }
  891. /**
  892. * Set or retrieve response content MIME type
  893. *
  894. * @deprecated
  895. * @param string $type
  896. * @return BResponse|string
  897. */
  898. public function contentType($type=BNULL)
  899. {
  900. if (BNULL===$type) {
  901. return $this->_contentType;
  902. }
  903. $this->_contentType = $type;
  904. return $this;
  905. }
  906. public function setContentType($type)
  907. {
  908. $this->_contentType = $type;
  909. return $this;
  910. }
  911. public function getContentType()
  912. {
  913. return $this->_contentType;
  914. }
  915. /**
  916. * Set or retrieve response content prefix string
  917. *
  918. * @deprecated
  919. * @param string $string
  920. * @return BResponse|string
  921. */
  922. public function contentPrefix($string=BNULL)
  923. {
  924. if (BNULL===$string) {
  925. return $this->_contentPrefix;
  926. }
  927. $this->_contentPrefix = $string;
  928. return $this;
  929. }
  930. public function setContentPrefix($string)
  931. {
  932. $this->_contentPrefix = $string;
  933. return $this;
  934. }
  935. public function getContentPrefix()
  936. {
  937. return $this->_contentPrefix;
  938. }
  939. /**
  940. * Set or retrieve response content suffix string
  941. *
  942. * @deprecated
  943. * @param string $string
  944. * @return BResponse|string
  945. */
  946. public function contentSuffix($string=BNULL)
  947. {
  948. if (BNULL===$string) {
  949. return $this->_contentSuffix;
  950. }
  951. $this->_contentSuffix = $string;
  952. return $this;
  953. }
  954. public function setContentSuffix($string)
  955. {
  956. $this->_contentSuffix = $string;
  957. return $this;
  958. }
  959. public function getContentSuffix()
  960. {
  961. return $this->_contentSuffix;
  962. }
  963. /**
  964. * Send json data as a response (for json API implementation)
  965. *
  966. * Supports JSON-P
  967. *
  968. * @param mixed $data
  969. */
  970. public function json($data)
  971. {
  972. $response = BUtil::toJson($data);
  973. $callback = BRequest::i()->get('callback');
  974. if ($callback) {
  975. $response = $callback.'('.$response.')';
  976. }
  977. $this->setContentType('application/json')->set($response)->render();
  978. }
  979. public function fileContentType($fileName)
  980. {
  981. $type = 'application/octet-stream';
  982. switch (strtolower(pathinfo($fileName, PATHINFO_EXTENSION))) {
  983. case 'jpeg': case 'jpg': $type = 'image/jpg'; break;
  984. case 'png': $type = 'image/png'; break;
  985. case 'gif': $type = 'image/gif'; break;
  986. }
  987. return $type;
  988. }
  989. /**
  990. * Send file download to client
  991. *
  992. * @param $source
  993. * @param null $fileName
  994. * @param string $disposition
  995. * @internal param string $filename
  996. * @return exit
  997. */
  998. public function sendFile($source, $fileName=null, $disposition='attachment')
  999. {
  1000. BSession::i()->close();
  1001. if (!$fileName) {
  1002. $fileName = basename($source);
  1003. }
  1004. header('Pragma: public');
  1005. header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
  1006. header('Content-Length: ' . filesize($source));
  1007. header('Last-Modified: ' . date('r'));
  1008. header('Content-Type: '. $this->fileContentType($fileName));
  1009. header('Content-Disposition: '.$disposition.'; filename=' . $fileName);
  1010. //echo file_get_contents($source);
  1011. $fs = fopen($source, 'rb');
  1012. $fd = fopen('php://output', 'wb');
  1013. while (!feof($fs)) fwrite($fd, fread($fs, 8192));
  1014. fclose($fs);
  1015. fclose($fd);
  1016. $this->shutdown(__METHOD__);
  1017. }
  1018. /**
  1019. * Send text content as a file download to client
  1020. *
  1021. * @param string $content
  1022. * @param string $fileName
  1023. * @param string $disposition
  1024. * @return exit
  1025. */
  1026. public function sendContent($content, $fileName='download.txt', $disposition='attachment')
  1027. {
  1028. BSession::i()->close();
  1029. header('Pragma: public');
  1030. header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
  1031. header('Content-Type: '.$this->fileContentType($fileName));
  1032. header('Content-Length: ' . strlen($content));
  1033. header('Last-Modified: ' . date('r'));
  1034. header('Content-Disposition: '.$disposition.'; filename=' . $fileName);
  1035. echo $content;
  1036. $this->shutdown(__METHOD__);
  1037. }
  1038. /**
  1039. * Send status response to client
  1040. *
  1041. * @param int $status Status code number
  1042. * @param string $message Message to be sent to client
  1043. * @param bool|string $output Proceed to output content and exit
  1044. * @return BResponse|exit
  1045. */
  1046. public function status($status, $message=null, $output=true)
  1047. {
  1048. if (is_null($message)) {
  1049. if (!empty(static::$_httpStatuses[$status])) {
  1050. $message = static::$_httpStatuses[$status];
  1051. } else {
  1052. $message = 'Unknown';
  1053. }
  1054. }
  1055. $protocol = BRequest::i()->serverProtocol();
  1056. header("{$protocol} {$status} {$message}");
  1057. header("Status: {$status} {$message}");
  1058. if (is_string($output)) {
  1059. echo $output;
  1060. exit;
  1061. } elseif ($output) {
  1062. $this->output();
  1063. }
  1064. return $this;
  1065. }
  1066. /**
  1067. * Output the response to client
  1068. *
  1069. * @param string $type Optional content type
  1070. * @return exit
  1071. */
  1072. public function output($type=null)
  1073. {
  1074. if (!is_null($type)) {
  1075. $this->setContentType($type);
  1076. }
  1077. //BSession::i()->close();
  1078. header('Content-Type: '.$this->_contentType.'; charset='.$this->_charset);
  1079. if ($this->_contentType=='application/json') {
  1080. if (!empty($this->_content)) {
  1081. $this->_content = is_string($this->_content) ? $this->_content : BUtil::toJson($this->_content);
  1082. }
  1083. } elseif (is_null($this->_content)) {
  1084. $this->_content = BLayout::i()->render();
  1085. }
  1086. BEvents::i()->fire(__METHOD__.':before', array('content'=>&$this->_content));
  1087. if ($this->_contentPrefix) {
  1088. echo $this->_contentPrefix;
  1089. }
  1090. if ($this->_content) {
  1091. echo $this->_content;
  1092. }
  1093. if ($this->_contentSuffix) {
  1094. echo $this->_contentSuffix;
  1095. }
  1096. BEvents::i()->fire(__METHOD__.':after', array('content'=>$this->_content));
  1097. $this->shutdown(__METHOD__);
  1098. }
  1099. /**
  1100. * Alias for output
  1101. *
  1102. */
  1103. public function render()
  1104. {
  1105. $this->output();
  1106. }
  1107. /**
  1108. * Redirect browser to another URL
  1109. *
  1110. * @param string $url URL to redirect
  1111. * @param int $status Default 302, another possible value 301
  1112. */
  1113. public function redirect($url, $status=302)
  1114. {
  1115. BSession::i()->close();
  1116. $this->status($status, null, false);
  1117. if (!BUtil::isUrlFull($url)) {
  1118. $url = BApp::href($url);
  1119. }
  1120. header("Location: {$url}", null, $status);
  1121. $this->shutdown(__METHOD__);
  1122. }
  1123. public function httpsRedirect()
  1124. {
  1125. $this->redirect(str_replace('http://', 'https://', BRequest::i()->currentUrl()));
  1126. }
  1127. /**
  1128. * Send HTTP STS header
  1129. *
  1130. * @see http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
  1131. */
  1132. public function httpSTS()
  1133. {
  1134. header('Strict-Transport-Security: max-age=500; includeSubDomains');
  1135. return $this;
  1136. }
  1137. /**
  1138. * Enable CORS (Cross-Origin Resource Sharing)
  1139. *
  1140. * @param array $options
  1141. * @return BResponse
  1142. */
  1143. public function cors($options=array())
  1144. {
  1145. if (empty($options['origin'])) {
  1146. $options['origin'] = BRequest::i()->httpOrigin();
  1147. }
  1148. header('Access-Control-Allow-Origin: '.$options['origin']);
  1149. if (!empty($options['methods'])) {
  1150. header('Access-Control-Allow-Methods: '.$options['methods']);
  1151. }
  1152. if (!empty($options['credentials'])) {
  1153. header('Access-Control-Allow-Credentials: true');
  1154. }
  1155. if (!empty($options['headers'])) {
  1156. header('Access-Control-Allow-Headers: '.$options['headers']);
  1157. }
  1158. if (!empty($options['expose-headers'])) {
  1159. header('Access-Control-Expose-Headers: '.$options['expose-headers']);
  1160. }
  1161. if (!empty($options['age'])) {
  1162. header('Access-Control-Max-Age: '.$options['age']);
  1163. }
  1164. return $this;
  1165. }
  1166. public function nocache()
  1167. {
  1168. header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past
  1169. header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); // Current time
  1170. header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
  1171. header("Pragma: no-cache");
  1172. return $this;
  1173. }
  1174. public function startLongResponse($bypassBuffering = true)
  1175. {
  1176. // improve performance by not processing debug log
  1177. if (BDebug::is('DEBUG')) {
  1178. BDebug::mode('DEVELOPMENT');
  1179. }
  1180. // redundancy: avoid memory leakage from debug log
  1181. BDebug::level(BDebug::MEMORY, false);
  1182. // remove process timeout limitation
  1183. set_time_limit(0);
  1184. // output in real time
  1185. @ob_end_flush();
  1186. ob_implicit_flush();
  1187. // enable garbage collection
  1188. gc_enable();
  1189. // remove session lock
  1190. session_write_close();
  1191. // bypass initial webservice buffering
  1192. if ($bypassBuffering) {
  1193. echo str_pad('', 2000, ' ');
  1194. }
  1195. // continue in background if the browser request was interrupted
  1196. //ignore_user_abort(true);
  1197. return $this;
  1198. }
  1199. public function shutdown($lastMethod=null)
  1200. {
  1201. BEvents::i()->fire(__METHOD__, array('last_method'=>$lastMethod));
  1202. BSession::i()->close();
  1203. exit;
  1204. }
  1205. }
  1206. /**
  1207. * Front controller class to register and dispatch routes
  1208. */
  1209. class BRouting extends BClass
  1210. {
  1211. /**
  1212. * Array of routes
  1213. *
  1214. * @var array
  1215. */
  1216. protected $_routes = array();
  1217. protected $_routesRegex = array();
  1218. /**
  1219. * Partial route changes
  1220. *
  1221. * @var array
  1222. */
  1223. protected static $_routeChanges = array();
  1224. /**
  1225. * Current route node, empty if front controller dispatch wasn't run yet
  1226. *
  1227. * @var mixed
  1228. */
  1229. protected $_currentRoute;
  1230. /**
  1231. * Templates to generate URLs based on routes
  1232. *
  1233. * @var array
  1234. */
  1235. protected $_urlTemplates = array();
  1236. /**
  1237. * Current controller name
  1238. *
  1239. * @var string
  1240. */
  1241. protected $_controllerName;
  1242. /**
  1243. * Shortcut to help with IDE autocompletion
  1244. *
  1245. * @return BRouting
  1246. */
  1247. public static function i($new=false, array $args=array())
  1248. {
  1249. return BClassRegistry::i()->instance(__CLASS__, $args, !$new);
  1250. }
  1251. public function __construct()
  1252. {
  1253. $this->route('_ /noroute', 'BActionController.noroute', array(), null, false);
  1254. }
  1255. /**
  1256. * Change route part (usually 1st)
  1257. *
  1258. * @param string $partValue
  1259. * @param mixed $options
  1260. */
  1261. public function changeRoute($from, $opt)
  1262. {
  1263. if (!is_array($opt)) {
  1264. $opt = array('to'=>$opt);
  1265. }
  1266. $type = !empty($opt['type']) ? $opt['type'] : 'first';
  1267. unset($opt['type']);
  1268. $this->_routeChanges[$type][$from] = $opt;
  1269. return $this;
  1270. }
  1271. public static function processHref($href)
  1272. {
  1273. $href = ltrim($href, '/');
  1274. if (!empty(static::$_routeChanges['first'])) {
  1275. $rules = static::$_routeChanges['first'];
  1276. $parts = explode('/', $href, 2);
  1277. if (!empty($rules[$parts[0]])) {
  1278. $href = ($part0 = $rules[$parts[0]]['to'])
  1279. .($part0 && isset($parts[1]) ? '/' : '')
  1280. .(isset($parts[1]) ? $parts[1] : '');
  1281. }
  1282. }
  1283. return $href;
  1284. }
  1285. public function processRoutePath($route, $args=array())
  1286. {
  1287. if (!empty($args['module_name'])) {
  1288. $module = BModuleRegistry::i()->module($args['module_name']);
  1289. if ($module && ($prefix = $module->url_prefix)) {
  1290. $route = $prefix.$route;
  1291. }
  1292. }
  1293. #$route = static::processHref($route); // slower than alternative (replacing keys on saveRoute)
  1294. return $route;
  1295. }
  1296. /**
  1297. * Declare route
  1298. *
  1299. * @param string $route
  1300. * - "{GET|POST|DELETE|PUT|HEAD} /part1/part2/:param1"
  1301. * - "/prefix/*anything"
  1302. * - "/prefix/.action" : $args=array('_methods'=>array('create'=>'POST', ...))
  1303. * @param mixed $callback PHP callback
  1304. * @param array $args Route arguments
  1305. * @param string $name optional name for the route for URL templating
  1306. * @param bool $multiple
  1307. * @return BFrontController for chain linking
  1308. */
  1309. public function route($route, $callback=null, $args=null, $name=null, $multiple=true)
  1310. {
  1311. if (is_array($route)) {
  1312. foreach ($route as $a) {
  1313. if (is_null($callback)) {
  1314. $this->route($a[0], $a[1], isset($a[2])?$a[2]:null, isset($a[3])?$a[3]:null);
  1315. } else {
  1316. $this->route($a, $callback, $args, $name, $multiple);
  1317. }
  1318. }
  1319. return $this;
  1320. }
  1321. if (empty($args['module_name'])) {
  1322. $args['module_name'] = BModuleRegistry::i()->currentModuleName();
  1323. }
  1324. BDebug::debug('ROUTE '.$route);
  1325. if (empty($this->_routes[$route])) {
  1326. $this->_routes[$route] = new BRouteNode(array('route_name'=>$route));
  1327. }
  1328. $this->_routes[$route]->observe($callback, $args, $multiple);
  1329. if (!is_null($name)) {
  1330. $this->_urlTemplates[$name] = $route;
  1331. }
  1332. return $this;
  1333. }
  1334. /**
  1335. * Shortcut to $this->route() for GET http verb
  1336. * @param mixed $route
  1337. * @param mixed $callback
  1338. * @param array $args
  1339. * @param string $name
  1340. * @param bool $multiple
  1341. * @return BFrontController
  1342. */
  1343. public function get($route, $callback = null, $args = null, $name = null, $multiple = true)
  1344. {
  1345. return $this->_route($route, 'get', $callback, $args, $name, $multiple);
  1346. }
  1347. /**
  1348. * Shortcut to $this->route() for POST http verb
  1349. * @param mixed $route
  1350. * @param mixed $callback
  1351. * @param array $args
  1352. * @param string $name
  1353. * @param bool $multiple
  1354. * @return BFrontController
  1355. */
  1356. public function post($route, $callback = null, $args = null, $name = null, $multiple = true)
  1357. {
  1358. return $this->_route($route, 'post', $callback, $args, $name, $multiple);
  1359. }
  1360. /**
  1361. * Shortcut to $this->route() for PUT http verb
  1362. * @param mixed $route
  1363. * @param null $callback
  1364. * @param null $args
  1365. * @param null $name
  1366. * @param bool $multiple
  1367. * @return $this|BFrontController
  1368. */
  1369. public function put($route, $callback = null, $args = null, $name = null, $multiple = true)
  1370. {
  1371. return $this->_route($route, 'put', $callback, $args, $name, $multiple);
  1372. }
  1373. /**
  1374. * Shortcut to $this->route() for GET|POST|DELETE|PUT|HEAD http verbs
  1375. * @param mixed $route
  1376. * @param null $callback
  1377. * @param null $args
  1378. * @param null $name
  1379. * @param bool $multiple
  1380. * @return $this|BFrontController
  1381. */
  1382. public function any($route, $callback = null, $args = null, $name = null, $multiple = true)
  1383. {
  1384. return $this->_route($route, 'any', $callback, $args, $name, $multiple);
  1385. }
  1386. /**
  1387. * Process shortcut methods
  1388. * @param mixed $route
  1389. * @param string $verb
  1390. * @param null $callback
  1391. * @param null $args
  1392. * @param null $name
  1393. * @param bool $multiple
  1394. * @return $this|BFrontController
  1395. */
  1396. protected function _route($route, $verb, $callback = null, $args = null, $name = null, $multiple = true)
  1397. {
  1398. if (is_array($route)) {
  1399. foreach ($route as $a) {
  1400. if (is_null($callback)) {
  1401. $this->_route($a[0], $verb, $a[1], isset($a[2]) ? $a[2] : null, isset($a[3]) ? $a[3] : null);
  1402. } else {
  1403. $this->any($a, $verb, $callback, $args);
  1404. }
  1405. }
  1406. return $this;
  1407. }
  1408. $verb = strtoupper($verb);
  1409. $isRegex = false;
  1410. if ($route[0]==='^') {
  1411. $isRegex = true;
  1412. $route = substr($route, 1);
  1413. }
  1414. if ($verb==='GET' || $verb==='POST' || $verb==='PUT') {
  1415. $route = $verb.' '.$route;
  1416. } else {
  1417. if ($isRegex) {
  1418. $route = '(GET|POST|DELETE|PUT|HEAD) '.$route;
  1419. } else {
  1420. $route = 'GET|POST|DELETE|PUT|HEAD '.$route;
  1421. }
  1422. }
  1423. if ($isRegex) {
  1424. $route = '^'.$route;
  1425. }
  1426. return $this->route($route, $callback, $args, $name, $multiple);
  1427. }
  1428. public function findRoute($requestRoute=null)
  1429. {
  1430. if (is_null($requestRoute)) {
  1431. $requestRoute = BRequest::i()->rawPath();
  1432. }
  1433. if (strpos($requestRoute, ' ')===false) {
  1434. $requestRoute = BRequest::i()->method().' '.$requestRoute;
  1435. }
  1436. if (!empty($this->_routes[$requestRoute]) && $this->_routes[$requestRoute]->validObserver()) {
  1437. BDebug::debug('DIRECT ROUTE: '.$requestRoute);
  1438. return $this->_routes[$requestRoute];
  1439. }
  1440. BDebug::debug('FIND ROUTE: '.$requestRoute);
  1441. foreach ($this->_routes as $route) {
  1442. if ($route->match($requestRoute)) {
  1443. return $route;
  1444. }
  1445. }
  1446. return null;
  1447. }
  1448. /**
  1449. * Convert collected routes into tree
  1450. *
  1451. * @return BFrontController
  1452. */
  1453. public function processRoutes()
  1454. {
  1455. uasort($this->_routes, function($a, $b) {
  1456. $a1 = $a->num_parts;
  1457. $b1 = $b->num_parts;
  1458. $res = $a1<$b1 ? 1 : ($a1>$b1 ? -1 : 0);
  1459. if ($res != 0) {
  1460. #echo ' ** ('.$a->route_name.'):('.$b->route_name.'): '.$res.' ** <br>';
  1461. return $res;
  1462. }
  1463. $ap = (strpos($a->route_name, '/*') ? 10 : 0)+(strpos($a->route_name, '/.') ? 5 : 0)+(strpos($a->route_name, '/:') ? 1 : 0);
  1464. $bp = (strpos($b->route_name, '/*') ? 10 : 0)+(strpos($b->route_name, '/.') ? 5 : 0)+(strpos($b->route_name, '/:') ? 1 : 0);
  1465. #echo $a->route_name.' ('.$ap.'), '.$b->route_name.'('.$bp.')<br>';
  1466. return $ap === $bp ? 0 : ($ap < $bp ? -1 : 1 );
  1467. });
  1468. #echo "<pre>"; print_r($this->_routes); echo "</pre>";
  1469. return $this;
  1470. }
  1471. public function forward($from, $to, $args=array())
  1472. {
  1473. $args['target'] = $to;
  1474. $this->route($from, array($this, '_forwardCallback'), $args);
  1475. /*
  1476. $this->route($from, function($args) {
  1477. return array('forward'=>$this->processRoutePath($args['target'], $args));
  1478. }, $args);
  1479. */
  1480. return $this;
  1481. }
  1482. protected function _forwardCallback($args)
  1483. {
  1484. return $this->processRoutePath($args['target'], $args);
  1485. }
  1486. public function redirect($from, $to, $args=array())
  1487. {
  1488. $args['target'] = $to;
  1489. $this->route($from, array($this, 'redirectCallback'), $args);
  1490. return $this;
  1491. }
  1492. public function redirectCallback($args)
  1493. {
  1494. BResponse::i()->redirect(BApp::href($args['target']));
  1495. }
  1496. /**
  1497. * Retrieve current route node
  1498. *
  1499. */
  1500. public function currentRoute()
  1501. {
  1502. return $this->_currentRoute;
  1503. }
  1504. /**
  1505. * Dispatch current route
  1506. *
  1507. * @param string $requestRoute optional route for explicit route dispatch
  1508. * @return BFrontController
  1509. */
  1510. public function dispatch($requestRoute=null)
  1511. {
  1512. BEvents::i()->fire(__METHOD__.':before');
  1513. $this->processRoutes();
  1514. $attempts = 0;
  1515. $forward = false; // null: no forward, false: try next route, array: forward without new route
  1516. #echo "<pre>"; print_r($this->_routes); exit;
  1517. while (($attempts++<100) && (false===$forward || is_array($forward))) {
  1518. $route = $this->findRoute($requestRoute);
  1519. #echo "<pre>"; print_r($route); echo "</pre>";
  1520. if (!$route) {
  1521. $route = $this->findRoute('_ /noroute');
  1522. }
  1523. $this->_currentRoute = $route;
  1524. $forward = $route->dispatch();
  1525. #var_dump($forward); exit;
  1526. if (is_array($forward)) {
  1527. list($actionName, $forwardCtrlName, $params) = $forward;
  1528. $controllerName = $forwardCtrlName ? $forwardCtrlName : $route->controller_name;
  1529. $requestRoute = '_ /forward';
  1530. $this->route($requestRoute, $controllerName.'.'.$actionName, array(), null, false);
  1531. }
  1532. }
  1533. if ($attempts>=100) {
  1534. echo "<pre>"; print_r($route); echo "</pre>";
  1535. BDebug::error(BLocale::_('BFrontController: Reached 100 route iterations: %s', print_r($route,1)));
  1536. }
  1537. }
  1538. public function debug()
  1539. {
  1540. echo "<pre>"; print_r($this->_routes); echo "</pre>";
  1541. }
  1542. }
  1543. /**
  1544. * Alias for BRouting for older implementations
  1545. *
  1546. * @deprecated by BRouting
  1547. */
  1548. class BFrontController extends BRouting {}
  1549. /**
  1550. * Controller Route Node
  1551. */
  1552. class BRouteNode
  1553. {
  1554. /**
  1555. * Route flags
  1556. *
  1557. * @var array
  1558. */
  1559. protected $_flags = array();
  1560. /**
  1561. * Route Observers
  1562. *
  1563. * @var array(BRouteObserver)
  1564. */
  1565. protected $_observers = array();
  1566. public $controller_name;
  1567. public $action_idx;
  1568. public $action_name;
  1569. public $route_name;
  1570. public $regex;
  1571. public $num_parts; // specificity for sorting
  1572. public $params;
  1573. public $params_values;
  1574. public $multi_method;
  1575. public function __construct($args=array())
  1576. {
  1577. foreach ($args as $k=>$v) {
  1578. $this->$k = $v;
  1579. }
  1580. // convert route name into regex and save param references
  1581. if ($this->route_name[0]==='^') {
  1582. $this->regex = '#'.$this->route_name.'#';
  1583. return;
  1584. }
  1585. $a = explode(' ', $this->route_name);
  1586. if (sizeof($a)<2) {
  1587. throw new BException('Invalid route format: '.$this->route_name);
  1588. }
  1589. $this->multi_method = strpos($a[0], '|') !== false;
  1590. if ($a[1]==='/') {
  1591. $this->regex = '#^('.$a[0].') (/)$#';
  1592. } else {
  1593. $a1 = explode('/', trim($a[1], '/'));
  1594. $this->num_parts = sizeof($a1);
  1595. $paramId = 2;
  1596. foreach ($a1 as $i=>$k) {
  1597. $k0 = $k[0];
  1598. $part = '';
  1599. if ($k0==='?') {
  1600. $k = substr($k, 1);
  1601. $k0 = $k[0];
  1602. $part = '?';
  1603. }
  1604. if ($k0===':') { // optional param
  1605. $this->params[++$paramId] = substr($k, 1);
  1606. $part .= '([^/]*)';
  1607. } elseif ($k0==='!') { // required param
  1608. $this->params[++$paramId] = substr($k, 1);
  1609. $part .= '([^/]+)';
  1610. } elseif ($k0==='*') { // param until end of url
  1611. $this->params[++$paramId] = substr($k, 1);
  1612. $part .= '(.*)';
  1613. } elseif ($k0==='.') { // dynamic action
  1614. $this->params[++$paramId] = substr($k, 1);
  1615. $this->action_idx = $paramId;
  1616. $part .= '([a-zA-Z0-9_]*)';
  1617. } else {
  1618. //$part .= preg_quote($a1[$i]);
  1619. }
  1620. if (''!==$part) {
  1621. $a1[$i] = $part;
  1622. }
  1623. }
  1624. $this->regex = '#^('.$a[0].') (/'.join('/', $a1).'/?)$#'; // #...#i option?
  1625. #echo $this->regex.'<hr>';
  1626. }
  1627. }
  1628. public function match($route)
  1629. {
  1630. if (!preg_match($this->regex, $route, $match)) {
  1631. return false;
  1632. }
  1633. if (!$this->validObserver()) {
  1634. return false;
  1635. }
  1636. if ($this->action_idx) {
  1637. $this->action_name = !empty($match[$this->action_idx]) ? $match[$this->action_idx] : 'index';
  1638. }
  1639. if ($this->route_name[0]==='^') {
  1640. $this->params_values = $match;
  1641. } elseif ($this->params) {
  1642. $this->params_values = array();
  1643. foreach ($this->params as $i=>$p) {
  1644. $this->params_values[$p] = $match[$i];
  1645. }
  1646. }
  1647. return true;
  1648. }
  1649. /**
  1650. * Set route flag
  1651. *
  1652. * - ? - allow trailing slash
  1653. *
  1654. * @todo make use of it
  1655. *
  1656. * @param string $flag
  1657. * @param mixed $value
  1658. * @return BRouteNode
  1659. */
  1660. public function flag($flag, $value=true)
  1661. {
  1662. $this->_flags[$flag] = $value;
  1663. return $this;
  1664. }
  1665. /**
  1666. * Add an observer to the route node
  1667. *
  1668. * @param mixed $callback
  1669. * @param array $args
  1670. * @param boolean $multiple whether to allow multiple observers for the route
  1671. */
  1672. public function observe($callback, $args=null, $multiple=true)
  1673. {
  1674. $observer = new BRouteObserver(array(
  1675. 'callback' => $callback,
  1676. 'args' => $args,
  1677. 'route_node' => $this,
  1678. ));
  1679. if ($multiple) {
  1680. $this->_observers[] = $observer;
  1681. } else {
  1682. //$this->_observers = BUtil::arrayMerge($this->_observers[0], $observer);
  1683. $this->_observers = array($observer);
  1684. }
  1685. return $this;
  1686. }
  1687. /**
  1688. * Retrieve next valid (not skipped) observer
  1689. *
  1690. * @return BRouteObserver
  1691. */
  1692. public function validObserver()
  1693. {
  1694. foreach ($this->_observers as $o) {
  1695. if (!$o->skip) return $o;
  1696. }
  1697. return null;
  1698. }
  1699. /**
  1700. * Try to dispatch valid observers
  1701. *
  1702. * Will try to call observers in this node in order of save
  1703. *
  1704. * @return array|boolean forward info
  1705. */
  1706. public function dispatch()
  1707. {
  1708. $attempts = 0;
  1709. $observer = $this->validObserver();
  1710. while ((++$attempts<100) && $observer) {
  1711. $forward = $observer->dispatch();
  1712. if (is_array($forward)) {
  1713. return $forward;
  1714. } elseif ($forward===false) {
  1715. $observer->skip = true;
  1716. $observer = $this->validObserver();
  1717. } else {
  1718. return null;
  1719. }
  1720. }
  1721. if ($attempts>=100) {
  1722. BDebug::error(BLocale::_('BRouteNode: Reached 100 route iterations: %s', print_r($observer,1)));
  1723. }
  1724. return false;
  1725. }
  1726. public function __destruct()
  1727. {
  1728. unset($this->_observers, $this->_children, $this->_match);
  1729. }
  1730. }
  1731. /**
  1732. * Controller route observer
  1733. */
  1734. class BRouteObserver
  1735. {
  1736. /**
  1737. * Observer callback
  1738. *
  1739. * @var mixed
  1740. */
  1741. public $callback;
  1742. /**
  1743. * Callback arguments
  1744. *
  1745. * @var array
  1746. */
  1747. public $args;
  1748. /**
  1749. * Whether to skip the route when trying another
  1750. *
  1751. * @var boolean
  1752. */
  1753. public $skip;
  1754. /**
  1755. * Parent route node
  1756. *
  1757. * @var BRouteNode
  1758. */
  1759. public $route_node;
  1760. public function __construct($args)
  1761. {
  1762. foreach ($args as $k=>$v) {
  1763. $this->$k = $v;
  1764. }
  1765. }
  1766. /**
  1767. * Dispatch route node callback
  1768. *
  1769. * @return forward info
  1770. */
  1771. public function dispatch()
  1772. {
  1773. BModuleRegistry::i()->currentModule(!empty($this->args['module_name']) ? $this->args['module_name'] : null);
  1774. $node = $this->route_node;
  1775. BRequest::i()->initParams((array)$node->params_values);
  1776. if (is_string($this->callback) && $node->action_name) {
  1777. // prevent envoking action_index__POST methods directly
  1778. $actionNameArr = explode('__', $node->action_name, 2);
  1779. $this->callback .= '.'.$actionNameArr[0];
  1780. }
  1781. if (is_callable($this->callback)) {
  1782. return call_user_func($this->callback, $this->args);
  1783. }
  1784. if (is_string($this->callback)) {
  1785. foreach (array('.', '->') as $sep) {
  1786. $r = explode($sep, $this->callback);
  1787. if (sizeof($r)==2) {
  1788. $this->callback = $r;
  1789. break;
  1790. }
  1791. }
  1792. }
  1793. $actionName = '';
  1794. $controllerName = '';
  1795. if (is_array($this->callback)) {
  1796. $controllerName = $this->callback[0];
  1797. $node->controller_name = $controllerName;
  1798. $actionName = $this->callback[1];
  1799. }
  1800. #var_dump($controllerName, $actionName);
  1801. /** @var BActionController */
  1802. $controller = BClassRegistry::i()->instance($controllerName, array(), true);
  1803. return $controller->dispatch($actionName, $this->args);
  1804. }
  1805. public function __destruct()
  1806. {
  1807. unset($this->route_node, $this->callback, $this->args, $this->params);
  1808. }
  1809. }
  1810. /**
  1811. * Action controller class for route action declarations
  1812. */
  1813. class BActionController extends BClass
  1814. {
  1815. /**
  1816. * Current action name
  1817. *
  1818. * @var string
  1819. */
  1820. protected $_action;
  1821. /**
  1822. * Forward location. If set the dispatch will loop and forward to next action
  1823. *
  1824. * @var string|null
  1825. */
  1826. protected $_forward;
  1827. /**
  1828. * Prefix for action methods
  1829. *
  1830. * @var string
  1831. */
  1832. protected $_actionMethodPrefix = 'action_';
  1833. public function __construct()
  1834. {
  1835. }
  1836. /**
  1837. * Shortcut for fetching layout views
  1838. *
  1839. * @param string $viewname
  1840. * @return BView
  1841. */
  1842. public function view($viewname)
  1843. {
  1844. return BLayout::i()->view($viewname);
  1845. }
  1846. /**
  1847. * Dispatch action within the action controller class
  1848. *
  1849. * @param string $actionName
  1850. * @param array $args Action arguments
  1851. * @return forward information
  1852. */
  1853. public function dispatch($actionName, $args=array())
  1854. {
  1855. $this->_action = $actionName;
  1856. $this->_forward = null;
  1857. if (!$this->beforeDispatch($args)) {
  1858. return false;
  1859. }
  1860. if (!is_null($this->_forward)) {
  1861. return $this->_forward;
  1862. }
  1863. $authenticated = $this->authenticate($args);
  1864. if (!$authenticated && $actionName!=='unauthenticated') {
  1865. $this->forward('unauthenticated');
  1866. return $this->_forward;
  1867. }
  1868. if ($authenticated && !$this->authorize($args) && $actionName!=='unauthorized') {
  1869. $this->forward('unauthorized');
  1870. return $this->_forward;
  1871. }
  1872. $this->tryDispatch($actionName, $args);
  1873. if (is_null($this->_forward)) {
  1874. $this->afterDispatch($args);
  1875. }
  1876. return $this->_forward;
  1877. }
  1878. /**
  1879. * Try to dispatch action and catch exception if any
  1880. *
  1881. * @param string $actionName
  1882. * @param array $args
  1883. */
  1884. public function tryDispatch($actionName, $args)
  1885. {
  1886. if (!is_string($actionName) && is_callable($actionName)) {
  1887. try {
  1888. call_user_func($actionName);
  1889. } catch (Exception $e) {
  1890. BDebug::exceptionHandler($e);
  1891. $this->sendError($e->getMessage());
  1892. }
  1893. return $this;
  1894. }
  1895. $actionMethod = $this->_actionMethodPrefix.$actionName;
  1896. $reqMethod = BRequest::i()->method();
  1897. if ($reqMethod !== 'GET') {
  1898. $tmpMethod = $actionMethod.'__'.$reqMethod;
  1899. if (method_exists($this, $tmpMethod)) {
  1900. $actionMethod = $tmpMethod;
  1901. } elseif (BRouting::i()->currentRoute()->multi_method) {
  1902. $this->forward(false); // If route has multiple methods, require method suffix
  1903. }
  1904. }
  1905. //echo $actionMethod;exit;
  1906. if (!method_exists($this, $actionMethod)) {
  1907. $this->forward(false);
  1908. return $this;
  1909. }
  1910. try {
  1911. $this->$actionMethod($args);
  1912. } catch (Exception $e) {
  1913. //BDebug::exceptionHandler($e);
  1914. $this->sendError($e->getMessage());
  1915. }
  1916. return $this;
  1917. }
  1918. /**
  1919. * Forward to another action or retrieve current forward
  1920. *
  1921. * @param string $actionName
  1922. * @param string $controllerName
  1923. * @param array $params
  1924. * @return string|null|BActionController
  1925. */
  1926. public function forward($actionName=null, $controllerName=null, array $params=array())
  1927. {
  1928. if (false===$actionName) {
  1929. $this->_forward = false;
  1930. } else {
  1931. $this->_forward = array($actionName, $controllerName, $params);
  1932. }
  1933. return $this;
  1934. }
  1935. public function getForward()
  1936. {
  1937. return $this->_forward;
  1938. }
  1939. /**
  1940. * Authenticate logic for current action controller, based on arguments
  1941. *
  1942. * Use $this->_action to fetch current action
  1943. *
  1944. * @param array $args
  1945. */
  1946. public function authenticate($args=array())
  1947. {
  1948. return true;
  1949. }
  1950. /**
  1951. * Authorize logic for current action controller, based on arguments
  1952. *
  1953. * Use $this->_action to fetch current action
  1954. *
  1955. * @param array $args
  1956. */
  1957. public function authorize($args=array())
  1958. {
  1959. return true;
  1960. }
  1961. /**
  1962. * Execute before dispatch and return resutl
  1963. * If false, do not dispatch action, and either forward or default
  1964. *
  1965. * @return bool
  1966. */
  1967. public function beforeDispatch()
  1968. {
  1969. BEvents::i()->fire(__METHOD__);
  1970. return true;
  1971. }
  1972. /**
  1973. * Execute after dispatch
  1974. *
  1975. */
  1976. public function afterDispatch()
  1977. {
  1978. BEvents::i()->fire(__METHOD__);
  1979. }
  1980. /**
  1981. * Send error to the browser
  1982. *
  1983. * @param string $message to be in response
  1984. * @return exit
  1985. */
  1986. public function sendError($message)
  1987. {
  1988. BResponse::i()->set($message)->status(503);
  1989. }
  1990. /**
  1991. * Default unauthorized action
  1992. *
  1993. */
  1994. public function action_unauthenticated()
  1995. {
  1996. BResponse::i()->set("Unauthenticated")->status(401);
  1997. }
  1998. /**
  1999. * Default unauthorized action
  2000. *
  2001. */
  2002. public function action_unauthorized()
  2003. {
  2004. BResponse::i()->set("Unauthorized")->status(403);
  2005. }
  2006. /**
  2007. * Default not found action
  2008. *
  2009. */
  2010. public function action_noroute()
  2011. {
  2012. BResponse::i()->set("Route not found")->status(404);
  2013. }
  2014. /**
  2015. * Render output
  2016. *
  2017. * Final method to be called in standard action method
  2018. */
  2019. public function renderOutput()
  2020. {
  2021. BResponse::i()->output();
  2022. }
  2023. public function getAction()
  2024. {
  2025. return $this->_action;
  2026. }
  2027. public function getController()
  2028. {
  2029. return self::origClass();
  2030. }
  2031. public function viewProxy($viewPrefix, $defaultView='index', $hookName = 'main', $baseLayout = null)
  2032. {
  2033. $viewPrefix = trim($viewPrefix, '/').'/';
  2034. $page = BRequest::i()->params('view');
  2035. if (!$page) {
  2036. $page = $defaultView;
  2037. }
  2038. $view = $this->view($viewPrefix.$page);
  2039. if ($view instanceof BViewEmpty) {
  2040. $this->forward(false);
  2041. return false;
  2042. }
  2043. if ($baseLayout) {
  2044. $this->layout($baseLayout);
  2045. }
  2046. BLayout::i()->applyLayout('view-proxy')->applyLayout($viewPrefix.$page);
  2047. $view->useMetaData();
  2048. if (($root = BLayout::i()->view('root'))) {
  2049. $root->addBodyClass('page-'.$page);
  2050. }
  2051. BLayout::i()->hookView($hookName, $viewPrefix . $page);
  2052. if (!empty($metaData['http_status'])) {
  2053. BResponse::i()->status($metaData['http_status']);
  2054. }
  2055. return $page;
  2056. }
  2057. /**
  2058. * Translate string within controller action
  2059. *
  2060. * @param string $string
  2061. * @param array $params
  2062. * @param string $module if null, try to get current controller module
  2063. */
  2064. public function _($string, $params=array(), $module=null)
  2065. {
  2066. if (empty($module)) {
  2067. $module = BModuleRegistry::i()->currentModuleName();
  2068. }
  2069. return BLocale::_($string, $params, $module);
  2070. }
  2071. }