      1: <?php
      2:
      3: /**
      4:  *
      5:  * The front controller designed to optionally handle all requests
      6:  *
      7:  * @package alpha::controller::front
      8:  * @since 1.0
      9:  * @author John Collins <dev@alphaframework.org>
     10:  * @version $Id: FrontController.inc 1693 2013-12-09 23:33:24Z alphadevx $
     11:  * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
     12:  * @copyright Copyright (c) 2013, John Collins (founder of Alpha Framework).
     13:  * All rights reserved.
     14:  *
     15:  * <pre>
     16:  * Redistribution and use in source and binary forms, with or
     17:  * without modification, are permitted provided that the
     18:  * following conditions are met:
     19:  *
     20:  * * Redistributions of source code must retain the above
     21:  *   copyright notice, this list of conditions and the
     22:  *   following disclaimer.
     23:  * * Redistributions in binary form must reproduce the above
     24:  *   copyright notice, this list of conditions and the
     25:  *   following disclaimer in the documentation and/or other
     26:  *   materials provided with the distribution.
     27:  * * Neither the name of the Alpha Framework nor the names
     28:  *   of its contributors may be used to endorse or promote
     29:  *   products derived from this software without specific
     30:  *   prior written permission.
     31:  *
     32:  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
     33:  * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
     34:  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
     35:  * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     36:  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
     37:  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     38:  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
     39:  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     40:  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
     41:  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
     42:  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
     43:  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     44:  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     45:  * </pre>
     46:  *
     47:  */
     48: class FrontController {
     49:     /**
     50:      * The GET query string
     51:      *
     52:      * @var string
     53:      * @since 1.0
     54:      */
     55:     private $queryString;
     56:
     57:     /**
     58:      * The name of the page controller we want to invoke
     59:      *
     60:      * @var string
     61:      * @since 1.0
     62:      */
     63:     private $pageController;
     64:
     65:     /**
     66:      * Boolean to flag if the GET query string is encrypted or not
     67:      *
     68:      * @var boolean
     69:      * @since 1.0
     70:      */
     71:     private $encryptedQuery = false;
     72:
     73:     /**
     74:      * An array of controller alias
     75:      *
     76:      * @var array
     77:      * @since 1.0
     78:      */
     79:     private $controllerAlias = array();
     80:
     81:     /**
     82:      * An array of HTTP filters applied to each request to the front controller.  Each
     83:      * member must implement AlphaFilterInterface!
     84:      *
     85:      * @var array
     86:      * @since 1.0
     87:      */
     88:     private $filters = array();
     89:
     90:     /**
     91:      * The name of the current alias
     92:      *
     93:      * @var string
     94:      * @since 1.0
     95:      */
     96:     private $currentAlias;
     97:
     98:     /**
     99:      * Trace logger
    100:      *
    101:      * @var Logger
    102:      * @since 1.0
    103:      */
    104:     private static $logger = null;
    105:
    106:     /**
    107:      * The constructor method
    108:      *
    109:      * @throws ResourceNotFoundException
    110:      * @throws BadRequestException
    111:      * @since 1.0
    112:      */
    113:     public function __construct() {
    114:         self::$logger = new Logger('FrontController');
    115:
    116:         self::$logger->debug('>>__construct()');
    117:
    118:         global $config;
    119:
    120:         mb_internal_encoding('UTF-8');
    121:         mb_http_output('UTF-8');
    122:         mb_http_input('UTF-8');
    123:         ini_set('default_charset', 'utf-8');
    124:         if (!mb_check_encoding())
    125:             throw new BadRequestException('Request character encoding does not match expected UTF-8');
    126:
    127:         self::$logger->debug('Requested URL is ['.$_SERVER['REQUEST_URI'].']');
    128:
    129:         // direct calls to the front controller
    130:         if (isset($_GET['act'])) {
    131:             self::$logger->debug('Processing direct request to the front controller');
    132:             $this->pageController = $_GET['act'];
    133:         // calls to the front controller via mod_rewrite
    134:         }elseif($config->get('app.use.mod.rewrite') && !isset($_GET['tk'])) {
    135:             self::$logger->debug('Processing a mod_rewrite request');
    136:             $this->handleModRewriteRequests();
    137:         // direct calls to the front controller with an encrypted query string
    138:         }else{
    139:             if (!isset($_GET['tk'])) {
    140:                 self::$logger->warn('No controller action set for the front controller, URL is ['.$_SERVER['REQUEST_URI'].']');
    141:                 throw new ResourceNotFoundException('The file that you have requested cannot be found!');
    142:             }else{
    143:                 self::$logger->debug('Processing a direct request to the front controller with an encrypted token param');
    144:                 $this->setEncrypt(true);
    145:                 try {
    146:                     $this->decodeQuery();
    147:                     $this->populateGetVars();
    148:                     if(isset($_GET['act']))
    149:                         $this->pageController = $_GET['act'];
    150:                     else
    151:                         throw new SecurityException('No act param provided in the secure token!');
    152:                 }catch (SecurityException $e) {
    153:                     self::$logger->error('Error while attempting to decode a secure token in the FrontController: '.$e->getMessage());
    154:                     throw new ResourceNotFoundException('The file that you have requested cannot be found!');
    155:                 }
    156:             }
    157:         }
    158:
    159:         self::$logger->debug('<<__construct');
    160:     }
    161:
    162:     /**
    163:      * Sets the encryption flag
    164:      *
    165:      * @param boolean $encryptedQuery
    166:      * @since 1.0
    167:      */
    168:     public function setEncrypt($encryptedQuery) {
    169:         $this->encryptedQuery = $encryptedQuery;
    170:     }
    171:
    172:     /**
    173:      * Method to populate the global _GET and _REQUEST arrays with the decoded
    174:      * query string
    175:      *
    176:      * @since 1.0
    177:      */
    178:     private function populateGetVars() {
    179:
    180:         $pairs = explode('&', $this->queryString);
    181:
    182:         foreach($pairs as $pair) {
    183:             $keyValue = explode('=', $pair);
    184:             if(count($keyValue) == 2) {
    185:                 $_GET[$keyValue[0]] = $keyValue[1];
    186:                 $_REQUEST[$keyValue[0]] = $keyValue[1];
    187:             }
    188:         }
    189:     }
    190:
    191:     /**
    192:      * Static method for generating an absolute, secure URL for a page controller
    193:      *
    194:      * @param string $params
    195:      * @return string
    196:      * @since 1.0
    197:      */
    198:     public static function generateSecureURL($params) {
    199:         global $config;
    200:
    201:         if($config->get('app.use.mod.rewrite'))
    202:             return $config->get('app.url').'tk/'.FrontController::encodeQuery($params);
    203:         else
    204:             return $config->get('app.url').'?tk='.FrontController::encodeQuery($params);
    205:     }
    206:
    207:     /**
    208:      * Static method for encoding a query string
    209:      *
    210:      * @param string $queryString
    211:      * @return string
    212:      * @since 1.0
    213:      */
    214:     public static function encodeQuery($queryString) {
    215:         global $config;
    216:
    217:         $return = base64_encode(AlphaSecurityUtils::encrypt($queryString));
    218:         // remove any characters that are likely to cause trouble on a URL
    219:         $return = strtr($return, '+/', '-_');
    220:
    221:         return $return;
    222:     }
    223:
    224:     /**
    225:      * Method to decode the current query string
    226:      *
    227:      * @throws SecurityException
    228:      * @since 1.0
    229:      */
    230:     private function decodeQuery() {
    231:         global $config;
    232:
    233:         if (!isset($_GET['tk'])) {
    234:             throw new SecurityException('No token provided for the front controller!');
    235:         }else{
    236:             // replace any troublesome characters from the URL with the original values
    237:             $token = strtr($_GET['tk'], '-_', '+/');
    238:             $token = base64_decode($token);
    239:             $this->queryString = trim(AlphaSecurityUtils::decrypt($token));
    240:         }
    241:     }
    242:
    243:     /**
    244:      * Static method to return the decoded GET paramters from an encrytpted tk value
    245:      *
    246:      * @return string
    247:      * @since 1.0
    248:      */
    249:     public static function decodeQueryParams($tk) {
    250:         global $config;
    251:
    252:         // replace any troublesome characters from the URL with the original values
    253:         $token = strtr($tk, '-_', '+/');
    254:         $token = base64_decode($token);
    255:         $params = trim(AlphaSecurityUtils::decrypt($token));
    256:
    257:         return $params;
    258:     }
    259:
    260:     /**
    261:      * Static method to return the decoded GET paramters from an encrytpted tk value as an array of key/value pairs.
    262:      *
    263:      * @return array
    264:      * @since 1.0
    265:      */
    266:     public static function getDecodeQueryParams($tk) {
    267:         global $config;
    268:
    269:         // replace any troublesome characters from the URL with the original values
    270:         $token = strtr($tk, '-_', '+/');
    271:         $token = base64_decode($token);
    272:         $params = trim(AlphaSecurityUtils::decrypt($token));
    273:
    274:         $pairs = explode('&', $params);
    275:
    276:         $parameters = array();
    277:
    278:         foreach($pairs as $pair) {
    279:             $split = explode('=', $pair);
    280:             $parameters[$split[0]] = $split[1];
    281:         }
    282:
    283:         return $parameters;
    284:     }
    285:
    286:     /**
    287:      * Method to load the page controller
    288:      *
    289:      * @param boolean $allowRedirects Defaults to true, set to false if you want to prevent the front controller from redirecting the request
    290:      * @throws ResourceNotFoundException
    291:      * @since 1.0
    292:      */
    293:     public function loadController($allowRedirects = true) {
    294:         global $config;
    295:
    296:         if($allowRedirects && $config->get('app.check.installed') && $this->pageController != 'Install' && $this->pageController != 'Login') {
    297:             if(!AlphaDAO::isInstalled()) {
    298:                 self::$logger->info('Invoking the Install controller as the system DB is not installed...');
    299:                 $url = FrontController::generateSecureURL('act=Install');
    300:                 self::$logger->info('Redirecting to ['.$url.']');
    301:                 header('Location: '.$url);
    302:                 exit;
    303:             }
    304:         }
    305:
    306:         // first process any attached filters
    307:         foreach ($this->filters as $filter)
    308:             $filter->process();
    309:
    310:         if($allowRedirects) {
    311:             // if there is an alias configured for the above page controller, redirect there
    312:             if($config->get('app.force.front.controller') && $this->hasAlias($this->pageController)) {
    313:                 // make sure that it is not already an alias-based request to prevent re-direct loop
    314:                 if(empty($this->currentAlias)) {
    315:                     // set the correct HTTP header for the response
    316:                     header('HTTP/1.1 301 Moved Permanently');
    317:
    318:                     // see if there are any other GET params appart from the controller name
    319:                     if (count($_GET) > 1) {
    320:                         $keys = array_keys($_GET);
    321:                         $param = $_GET[$keys[1]];
    322:                         // if its a title then replace spaces with underscores in the URL
    323:                         if($keys[1] == 'title')
    324:                             $param = str_replace(' ','_',$param);
    325:
    326:                         $URL = $config->get('app.url').'/'.$this->getControllerAlias($this->pageController).'/'.
    327:                             $this->getControllerParam($this->pageController).$param;
    328:                     }else{
    329:                         $URL = $config->get('app.url').'/'.$this->getControllerAlias($this->pageController);
    330:                     }
    331:
    332:                     header('Location: '.$URL);
    333:                     exit;
    334:                 }
    335:             }
    336:         }
    337:
    338:         try {
    339:             AlphaController::loadControllerDef($this->pageController);
    340:             $pageController = new $this->pageController();
    341:
    342:             if(!empty($_POST)) {
    343:                 $pageController->doPOST($_REQUEST);
    344:             }else{
    345:                 $pageController->doGET($_GET);
    346:             }
    347:         }catch (LibraryNotInstalledException $e) {
    348:             self::$logger->warn($e->getMessage()."\nStacktrace:\n".$e->getTraceAsString()."\nRequest params:\n".var_export($_REQUEST, true)."\nRequested resource:\n".$_SERVER['REQUEST_URI']);
    349:             throw new LibraryNotInstalledException($e->getMessage());
    350:         }catch (ResourceNotAllowedException $e) {
    351:             self::$logger->warn($e->getMessage()."\nStacktrace:\n".$e->getTraceAsString()."\nRequest params:\n".var_export($_REQUEST, true)."\nRequested resource:\n".$_SERVER['REQUEST_URI']);
    352:             throw new ResourceNotAllowedException($e->getMessage());
    353:         }catch (ResourceNotFoundException $e) {
    354:             self::$logger->warn($e->getMessage()."\nStacktrace:\n".$e->getTraceAsString()."\nRequest params:\n".var_export($_REQUEST, true)."\nRequested resource:\n".$_SERVER['REQUEST_URI']);
    355:             throw new ResourceNotFoundException($e->getMessage());
    356:         }catch (IllegalArguementException $e) {
    357:             self::$logger->warn($e->getMessage()."\nStacktrace:\n".$e->getTraceAsString()."\nRequest params:\n".var_export($_REQUEST, true)."\nRequested resource:\n".$_SERVER['REQUEST_URI']);
    358:
    359:             if($config->get('security.client.temp.blacklist.filter.enabled')) {
    360:                 if(isset($_SERVER['HTTP_USER_AGENT']) && isset($_SERVER['REMOTE_ADDR']) && isset($_SERVER['REQUEST_URI'])) {
    361:                     $request = new BadRequestObject();
    362:                     $request->set('client', $_SERVER['HTTP_USER_AGENT']);
    363:                     $request->set('IP', $_SERVER['REMOTE_ADDR']);
    364:                     $request->set('requestedResource', $_SERVER['REQUEST_URI']);
    365:                     $request->save();
    366:                 }
    367:             }
    368:
    369:             throw new ResourceNotFoundException('The file that you have requested cannot be found!');
    370:         }catch (AlphaException $e) {
    371:             self::$logger->warn($e->getMessage()."\nStacktrace:\n".$e->getTraceAsString()."\nRequest params:\n".var_export($_REQUEST, true)."\nRequested resource:\n".$_SERVER['REQUEST_URI']);
    372:
    373:             if($config->get('security.client.temp.blacklist.filter.enabled')) {
    374:                 if(isset($_SERVER['HTTP_USER_AGENT']) && isset($_SERVER['REMOTE_ADDR']) && isset($_SERVER['REQUEST_URI'])) {
    375:                     $request = new BadRequestObject();
    376:                     $request->set('client', $_SERVER['HTTP_USER_AGENT']);
    377:                     $request->set('IP', $_SERVER['REMOTE_ADDR']);
    378:                     $request->set('requestedResource', $_SERVER['REQUEST_URI']);
    379:                     $request->save();
    380:                 }
    381:             }
    382:
    383:             throw new ResourceNotFoundException('The file that you have requested cannot be found!');
    384:         }
    385:     }
    386:
    387:     /**
    388:      * Used to register a controller alias to enable shorter URLs with mod_rewrite support enabled.  Note that
    389:      * only controllers with a single parameter are supported.
    390:      *
    391:      * @param string $controller The name of the page controller class
    392:      * @param string $alias The URL alias for the page controller
    393:      * @param string $param The name of the GET parameter on the alias URL request
    394:      * @since 1.0
    395:      */
    396:     public function registerAlias($controller, $alias, $param=null) {
    397:         $this->controllerAlias[$alias] = $controller;
    398:         if(isset($param))
    399:             $this->controllerAlias[$alias.'_param'] = $param;
    400:
    401:         // set up the page controller
    402:         $this->handleModRewriteRequests();
    403:     }
    404:
    405:     /**
    406:      * Check to see if an alias exists for the given alias name
    407:      *
    408:      * @param string $alias
    409:      * @return boolean
    410:      * @since 1.0
    411:      */
    412:     public function checkAlias($alias) {
    413:         if(array_key_exists($alias, $this->controllerAlias))
    414:             return true;
    415:         else
    416:             return false;
    417:     }
    418:
    419:     /**
    420:      * Check to see if an alias exists for the given controller name
    421:      *
    422:      * @param string $controller
    423:      * @return boolean
    424:      * @since 1.0
    425:      */
    426:     public function hasAlias($controller) {
    427:         if(in_array($controller, $this->controllerAlias))
    428:             return true;
    429:         else
    430:             return false;
    431:     }
    432:
    433:     /**
    434:      * Gets the full name of the controller for the given alias
    435:      *
    436:      * @param string $alias
    437:      * @return string
    438:      * @since 1.0
    439:      */
    440:     public function getAliasController($alias) {
    441:         if(array_key_exists($alias, $this->controllerAlias))
    442:             return $this->controllerAlias[$alias];
    443:     }
    444:
    445:     /**
    446:      * Gets the name of the alias for the given controller
    447:      *
    448:      * @param string $controller
    449:      * @return string
    450:      * @since 1.0
    451:      */
    452:     public function getControllerAlias($controller) {
    453:         if(in_array($controller, $this->controllerAlias)) {
    454:             $keys = array_keys($this->controllerAlias, $controller);
    455:             // there should only ever be one key per controller
    456:             return $keys[0];
    457:         }
    458:     }
    459:
    460:     /**
    461:      * Gets the parameter name expected in requests to the controller with the given alias
    462:      *
    463:      * @param string $alias
    464:      * @return string
    465:      * @since 1.0
    466:      */
    467:     public function getAliasParam($alias) {
    468:         if(array_key_exists($alias.'_param', $this->controllerAlias))
    469:             return $this->controllerAlias[$alias.'_param'];
    470:         else
    471:             return '';
    472:     }
    473:
    474:     /**
    475:      * Gets the parameter name expected in requests to the controller with the given controller name
    476:      *
    477:      * @param string $controller
    478:      * @return string
    479:      * @since 1.0
    480:      */
    481:     public function getControllerParam($controller) {
    482:         $alias = $this->getControllerAlias($controller);
    483:         if(array_key_exists($alias.'_param', $this->controllerAlias))
    484:             return $this->controllerAlias[$alias.'_param'];
    485:         else
    486:             return '';
    487:     }
    488:
    489:     /**
    490:      * Explodes the provided string into an array based on the array of delimiters provided
    491:      *
    492:      * @param string $string The string to explode.
    493:      * @param array $delimiters An array of delimiters.
    494:      * @todo move to string utils class
    495:      * @return array
    496:      * @since 1.2
    497:      */
    498:     private static function multipleExplode($string, $delimiters = array()){
    499:
    500:         $mainDelim=$delimiters[count($delimiters)-1];
    501:
    502:         array_pop($delimiters);
    503:
    504:         foreach($delimiters as $delimiter) {
    505:             $string = str_replace($delimiter, $mainDelim, $string);
    506:         }
    507:
    508:         $result = explode($mainDelim, $string);
    509:
    510:         return $result;
    511:     }
    512:
    513:     /**
    514:      * Handles all of the rules for mod_rewrite style URL parsing
    515:      *
    516:      * @since 1.0
    517:      */
    518:     private function handleModRewriteRequests() {
    519:         self::$logger->debug('>>handleModRewriteRequests');
    520:         global $config;
    521:
    522:         // strip off the system URL from the request URL
    523:         $request = str_replace($config->get('app.url'), '', 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
    524:         self::$logger->debug('$request is ['.$request.']');
    525:         $params = self::multipleExplode($request, array('/','?','&'));
    526:         self::$logger->debug('$params are ['.var_export($params, true).']');
    527:
    528:         try {
    529:             // first param will always be the controller alias
    530:             if(empty($this->currentAlias) && !empty($params[0]))
    531:                 $this->currentAlias = $params[0];
    532:
    533:             // check to see if we can load the page controller without an alias
    534:             AlphaController::loadControllerDef($params[0]);
    535:             self::$logger->debug('Page controller name set on the request URL is ['.$params[0].']');
    536:             $this->pageController = $params[0];
    537:         }catch (IllegalArguementException $iae) {
    538:             // handle request with alias
    539:             self::$logger->debug('The supplied controller alias is ['.$this->currentAlias.']');
    540:
    541:             // check to see if the controller is an alias for something
    542:             if($this->checkAlias($this->currentAlias)) {
    543:                 $this->pageController = $this->getAliasController($this->currentAlias);
    544:                 self::$logger->debug('Page controller name obtained from the URL alias is ['.$this->pageController.']');
    545:
    546:                 if(isset($params[1])) {
    547:                     if(!empty($_POST))
    548:                         $_REQUEST[$this->getAliasParam($this->currentAlias)] = $params[1];
    549:                     else
    550:                         $_GET[$this->getAliasParam($this->currentAlias)] = $params[1];
    551:                 }
    552:             }
    553:         }
    554:
    555:         self::$logger->debug('$params are ['.var_export($params, true).']');
    556:         self::$logger->debug('currentAlias is ['.$this->currentAlias.']');
    557:
    558:         // now populate the _GET vars
    559:         if($this->currentAlias == 'tk') {
    560:             self::$logger->debug('Setting the GET vars for a mod_rewrite request with a tk param');
    561:             $this->setEncrypt(true);
    562:             $this->queryString = FrontController::decodeQueryParams($params[1]);
    563:             $_GET['tk'] = $params[1];
    564:             $this->populateGetVars();
    565:             $this->pageController = $_GET['act'];
    566:         }else{
    567:             $count = count($params);
    568:
    569:             for($i = 1; $i < $count; $i+=2) {
    570:                 if(isset($params[$i+1])) {
    571:                     if(!empty($_POST))
    572:                         $_REQUEST[$params[$i]] = $params[$i+1];
    573:                     else
    574:                         $_GET[$params[$i]] = $params[$i+1];
    575:                 }
    576:             }
    577:         }
    578:
    579:         self::$logger->debug('$_GET is ['.var_export($_GET, true).']');
    580:         self::$logger->debug('<<handleModRewriteRequests');
    581:     }
    582:
    583:     /**
    584:      * Getter for the page controller
    585:      *
    586:      * @return string
    587:      * @since 1.0
    588:      */
    589:     public function getPageController() {
    590:         return $this->pageController;
    591:     }
    592:
    593:     /**
    594:      * Add the supplied filter object to the list of filters ran on each request to the front controller
    595:      *
    596:      * @param AlphaFilterInterface $filterObject
    597:      * @throws IllegalArguementException
    598:      * @since 1.0
    599:      */
    600:     public function registerFilter($filterObject) {
    601:         if($filterObject instanceof AlphaFilterInterface)
    602:             array_push($this->filters, $filterObject);
    603:         else
    604:             throw new IllegalArguementException('Supplied filter object is not a valid AlphaFilterInterface instance!');
    605:     }
    606:
    607:     /**
    608:      * Returns the array of filters currently attached to this FrontController
    609:      *
    610:      * @return array
    611:      * @since 1.0
    612:      */
    613:     public function getFilters() {
    614:         return $this->filters;
    615:     }
    616: }
    617:
    618: ?>