PageRenderTime 64ms CodeModel.GetById 9ms app.highlight 42ms RepoModel.GetById 1ms app.codeStats 1ms

/com/controller.php

http://github.com/unirgy/buckyball
PHP | 2261 lines | 1342 code | 186 blank | 733 comment | 182 complexity | 95f55a4bc3a005edd6f694998283f981 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

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

Large files files are truncated, but you can click here to view the full file