      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 1670 2013-09-12 20:41:08Z alphadevx $
     11:  * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
     12:  * @copyright Copyright (c) 2012, 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:      * @since 1.0
    111:      */
    112:     public function __construct() {
    113:         self::$logger = new Logger('FrontController');
    114:
    115:         self::$logger->debug('>>__construct()');
    116:
    117:         global $config;
    118:
    119:         self::$logger->debug('Requested URL is ['.$_SERVER['REQUEST_URI'].']');
    120:
    121:         // direct calls to the front controller
    122:         if (isset($_GET['act'])) {
    123:             self::$logger->debug('Processing direct request to the front controller');
    124:             $this->pageController = $_GET['act'];
    125:         // calls to the front controller via mod_rewrite
    126:         }elseif($config->get('app.use.mod.rewrite') && !isset($_GET['tk'])) {
    127:             self::$logger->debug('Processing a mod_rewrite request');
    128:             $this->handleModRewriteRequests();
    129:         // direct calls to the front controller with an encrypted query string
    130:         }else{
    131:             if (!isset($_GET['tk'])) {
    132:                 self::$logger->warn('No controller action set for the front controller, URL is ['.$_SERVER['REQUEST_URI'].']');
    133:                 throw new ResourceNotFoundException('The file that you have requested cannot be found!');
    134:             }else{
    135:                 self::$logger->debug('Processing a direct request to the front controller with an encrypted token param');
    136:                 $this->setEncrypt(true);
    137:                 try {
    138:                     $this->decodeQuery();
    139:                     $this->populateGetVars();
    140:                     if(isset($_GET['act']))
    141:                         $this->pageController = $_GET['act'];
    142:                     else
    143:                         throw new SecurityException('No act param provided in the secure token!');
    144:                 }catch (SecurityException $e) {
    145:                     self::$logger->error('Error while attempting to decode a secure token in the FrontController: '.$e->getMessage());
    146:                     throw new ResourceNotFoundException('The file that you have requested cannot be found!');
    147:                 }
    148:             }
    149:         }
    150:
    151:         self::$logger->debug('<<__construct');
    152:     }
    153:
    154:     /**
    155:      * Sets the encryption flag
    156:      *
    157:      * @param boolean $encryptedQuery
    158:      * @since 1.0
    159:      */
    160:     public function setEncrypt($encryptedQuery) {
    161:         $this->encryptedQuery = $encryptedQuery;
    162:     }
    163:
    164:     /**
    165:      * Method to populate the global _GET and _REQUEST arrays with the decoded
    166:      * query string
    167:      *
    168:      * @since 1.0
    169:      */
    170:     private function populateGetVars() {
    171:
    172:         $pairs = explode('&', $this->queryString);
    173:
    174:         foreach($pairs as $pair) {
    175:             $keyValue = explode('=', $pair);
    176:             if(count($keyValue) == 2) {
    177:                 $_GET[$keyValue[0]] = $keyValue[1];
    178:                 $_REQUEST[$keyValue[0]] = $keyValue[1];
    179:             }
    180:         }
    181:     }
    182:
    183:     /**
    184:      * Static method for generating an absolute, secure URL for a page controller
    185:      *
    186:      * @param string $params
    187:      * @return string
    188:      * @since 1.0
    189:      */
    190:     public static function generateSecureURL($params) {
    191:         global $config;
    192:
    193:         if($config->get('app.use.mod.rewrite'))
    194:             return $config->get('app.url').'tk/'.FrontController::encodeQuery($params);
    195:         else
    196:             return $config->get('app.url').'?tk='.FrontController::encodeQuery($params);
    197:     }
    198:
    199:     /**
    200:      * Static method for encoding a query string
    201:      *
    202:      * @param string $queryString
    203:      * @return string
    204:      * @since 1.0
    205:      */
    206:     public static function encodeQuery($queryString) {
    207:         global $config;
    208:
    209:         $return = base64_encode(AlphaSecurityUtils::encrypt($queryString));
    210:         // remove any characters that are likely to cause trouble on a URL
    211:         $return = strtr($return, '+/', '-_');
    212:
    213:         return $return;
    214:     }
    215:
    216:     /**
    217:      * Method to decode the current query string
    218:      *
    219:      * @throws SecurityException
    220:      * @since 1.0
    221:      */
    222:     private function decodeQuery() {
    223:         global $config;
    224:
    225:         if (!isset($_GET['tk'])) {
    226:             throw new SecurityException('No token provided for the front controller!');
    227:         }else{
    228:             // replace any troublesome characters from the URL with the original values
    229:             $token = strtr($_GET['tk'], '-_', '+/');
    230:             $token = base64_decode($token);
    231:             $this->queryString = trim(AlphaSecurityUtils::decrypt($token));
    232:         }
    233:     }
    234:
    235:     /**
    236:      * Static method to return the decoded GET paramters from an encrytpted tk value
    237:      *
    238:      * @return string
    239:      * @since 1.0
    240:      */
    241:     public static function decodeQueryParams($tk) {
    242:         global $config;
    243:
    244:         // replace any troublesome characters from the URL with the original values
    245:         $token = strtr($tk, '-_', '+/');
    246:         $token = base64_decode($token);
    247:         $params = trim(AlphaSecurityUtils::decrypt($token));
    248:
    249:         return $params;
    250:     }
    251:
    252:     /**
    253:      * Static method to return the decoded GET paramters from an encrytpted tk value as an array of key/value pairs.
    254:      *
    255:      * @return array
    256:      * @since 1.0
    257:      */
    258:     public static function getDecodeQueryParams($tk) {
    259:         global $config;
    260:
    261:         // replace any troublesome characters from the URL with the original values
    262:         $token = strtr($tk, '-_', '+/');
    263:         $token = base64_decode($token);
    264:         $params = trim(AlphaSecurityUtils::decrypt($token));
    265:
    266:         $pairs = explode('&', $params);
    267:
    268:         $parameters = array();
    269:
    270:         foreach($pairs as $pair) {
    271:             $split = explode('=', $pair);
    272:             $parameters[$split[0]] = $split[1];
    273:         }
    274:
    275:         return $parameters;
    276:     }
    277:
    278:     /**
    279:      * Method to load the page controller
    280:      *
    281:      * @param boolean $allowRedirects Defaults to true, set to false if you want to prevent the front controller from redirecting the request
    282:      * @throws ResourceNotFoundException
    283:      * @since 1.0
    284:      */
    285:     public function loadController($allowRedirects = true) {
    286:         global $config;
    287:
    288:         if($allowRedirects && $config->get('app.check.installed') && $this->pageController != 'Install' && $this->pageController != 'Login') {
    289:             if(!AlphaDAO::isInstalled()) {
    290:                 self::$logger->info('Invoking the Install controller as the system DB is not installed...');
    291:                 $url = FrontController::generateSecureURL('act=Install');
    292:                 self::$logger->info('Redirecting to ['.$url.']');
    293:                 header('Location: '.$url);
    294:                 exit;
    295:             }
    296:         }
    297:
    298:         // first process any attached filters
    299:         foreach ($this->filters as $filter)
    300:             $filter->process();
    301:
    302:         if($allowRedirects) {
    303:             // if there is an alias configured for the above page controller, redirect there
    304:             if($config->get('app.force.front.controller') && $this->hasAlias($this->pageController)) {
    305:                 // make sure that it is not already an alias-based request to prevent re-direct loop
    306:                 if(empty($this->currentAlias)) {
    307:                     // set the correct HTTP header for the response
    308:                     header('HTTP/1.1 301 Moved Permanently');
    309:
    310:                     // see if there are any other GET params appart from the controller name
    311:                     if (count($_GET) > 1) {
    312:                         $keys = array_keys($_GET);
    313:                         $param = $_GET[$keys[1]];
    314:                         // if its a title then replace spaces with underscores in the URL
    315:                         if($keys[1] == 'title')
    316:                             $param = str_replace(' ','_',$param);
    317:
    318:                         $URL = $config->get('app.url').'/'.$this->getControllerAlias($this->pageController).'/'.
    319:                             $this->getControllerParam($this->pageController).$param;
    320:                     }else{
    321:                         $URL = $config->get('app.url').'/'.$this->getControllerAlias($this->pageController);
    322:                     }
    323:
    324:                     header('Location: '.$URL);
    325:                     exit;
    326:                 }
    327:             }
    328:         }
    329:
    330:         try {
    331:             AlphaController::loadControllerDef($this->pageController);
    332:             $pageController = new $this->pageController();
    333:
    334:             if(!empty($_POST)) {
    335:                 $pageController->doPOST($_REQUEST);
    336:             }else{
    337:                 $pageController->doGET($_GET);
    338:             }
    339:         }catch (LibraryNotInstalledException $e) {
    340:             self::$logger->warn($e->getMessage()."\nStacktrace:\n".$e->getTraceAsString()."\nRequest params:\n".var_export($_REQUEST, true)."\nRequested resource:\n".$_SERVER['REQUEST_URI']);
    341:             throw new LibraryNotInstalledException($e->getMessage());
    342:         }catch (ResourceNotAllowedException $e) {
    343:             self::$logger->warn($e->getMessage()."\nStacktrace:\n".$e->getTraceAsString()."\nRequest params:\n".var_export($_REQUEST, true)."\nRequested resource:\n".$_SERVER['REQUEST_URI']);
    344:             throw new ResourceNotAllowedException($e->getMessage());
    345:         }catch (ResourceNotFoundException $e) {
    346:             self::$logger->warn($e->getMessage()."\nStacktrace:\n".$e->getTraceAsString()."\nRequest params:\n".var_export($_REQUEST, true)."\nRequested resource:\n".$_SERVER['REQUEST_URI']);
    347:             throw new ResourceNotFoundException($e->getMessage());
    348:         }catch (IllegalArguementException $e) {
    349:             self::$logger->warn($e->getMessage()."\nStacktrace:\n".$e->getTraceAsString()."\nRequest params:\n".var_export($_REQUEST, true)."\nRequested resource:\n".$_SERVER['REQUEST_URI']);
    350:
    351:             if($config->get('security.client.temp.blacklist.filter.enabled')) {
    352:                 if(isset($_SERVER['HTTP_USER_AGENT']) && isset($_SERVER['REMOTE_ADDR']) && isset($_SERVER['REQUEST_URI'])) {
    353:                     $request = new BadRequestObject();
    354:                     $request->set('client', $_SERVER['HTTP_USER_AGENT']);
    355:                     $request->set('IP', $_SERVER['REMOTE_ADDR']);
    356:                     $request->set('requestedResource', $_SERVER['REQUEST_URI']);
    357:                     $request->save();
    358:                 }
    359:             }
    360:
    361:             throw new ResourceNotFoundException('The file that you have requested cannot be found!');
    362:         }catch (AlphaException $e) {
    363:             self::$logger->warn($e->getMessage()."\nStacktrace:\n".$e->getTraceAsString()."\nRequest params:\n".var_export($_REQUEST, true)."\nRequested resource:\n".$_SERVER['REQUEST_URI']);
    364:
    365:             if($config->get('security.client.temp.blacklist.filter.enabled')) {
    366:                 if(isset($_SERVER['HTTP_USER_AGENT']) && isset($_SERVER['REMOTE_ADDR']) && isset($_SERVER['REQUEST_URI'])) {
    367:                     $request = new BadRequestObject();
    368:                     $request->set('client', $_SERVER['HTTP_USER_AGENT']);
    369:                     $request->set('IP', $_SERVER['REMOTE_ADDR']);
    370:                     $request->set('requestedResource', $_SERVER['REQUEST_URI']);
    371:                     $request->save();
    372:                 }
    373:             }
    374:
    375:             throw new ResourceNotFoundException('The file that you have requested cannot be found!');
    376:         }
    377:     }
    378:
    379:     /**
    380:      * Used to register a controller alias to enable shorter URLs with mod_rewrite support enabled.  Note that
    381:      * only controllers with a single parameter are supported.
    382:      *
    383:      * @param string $controller The name of the page controller class
    384:      * @param string $alias The URL alias for the page controller
    385:      * @param string $param The name of the GET parameter on the alias URL request
    386:      * @since 1.0
    387:      */
    388:     public function registerAlias($controller, $alias, $param=null) {
    389:         $this->controllerAlias[$alias] = $controller;
    390:         if(isset($param))
    391:             $this->controllerAlias[$alias.'_param'] = $param;
    392:
    393:         // set up the page controller
    394:         $this->handleModRewriteRequests();
    395:     }
    396:
    397:     /**
    398:      * Check to see if an alias exists for the given alias name
    399:      *
    400:      * @param string $alias
    401:      * @return boolean
    402:      * @since 1.0
    403:      */
    404:     public function checkAlias($alias) {
    405:         if(array_key_exists($alias, $this->controllerAlias))
    406:             return true;
    407:         else
    408:             return false;
    409:     }
    410:
    411:     /**
    412:      * Check to see if an alias exists for the given controller name
    413:      *
    414:      * @param string $controller
    415:      * @return boolean
    416:      * @since 1.0
    417:      */
    418:     public function hasAlias($controller) {
    419:         if(in_array($controller, $this->controllerAlias))
    420:             return true;
    421:         else
    422:             return false;
    423:     }
    424:
    425:     /**
    426:      * Gets the full name of the controller for the given alias
    427:      *
    428:      * @param string $alias
    429:      * @return string
    430:      * @since 1.0
    431:      */
    432:     public function getAliasController($alias) {
    433:         if(array_key_exists($alias, $this->controllerAlias))
    434:             return $this->controllerAlias[$alias];
    435:     }
    436:
    437:     /**
    438:      * Gets the name of the alias for the given controller
    439:      *
    440:      * @param string $controller
    441:      * @return string
    442:      * @since 1.0
    443:      */
    444:     public function getControllerAlias($controller) {
    445:         if(in_array($controller, $this->controllerAlias)) {
    446:             $keys = array_keys($this->controllerAlias, $controller);
    447:             // there should only ever be one key per controller
    448:             return $keys[0];
    449:         }
    450:     }
    451:
    452:     /**
    453:      * Gets the parameter name expected in requests to the controller with the given alias
    454:      *
    455:      * @param string $alias
    456:      * @return string
    457:      * @since 1.0
    458:      */
    459:     public function getAliasParam($alias) {
    460:         if(array_key_exists($alias.'_param', $this->controllerAlias))
    461:             return $this->controllerAlias[$alias.'_param'];
    462:         else
    463:             return '';
    464:     }
    465:
    466:     /**
    467:      * Gets the parameter name expected in requests to the controller with the given controller name
    468:      *
    469:      * @param string $controller
    470:      * @return string
    471:      * @since 1.0
    472:      */
    473:     public function getControllerParam($controller) {
    474:         $alias = $this->getControllerAlias($controller);
    475:         if(array_key_exists($alias.'_param', $this->controllerAlias))
    476:             return $this->controllerAlias[$alias.'_param'];
    477:         else
    478:             return '';
    479:     }
    480:
    481:     /**
    482:      * Explodes the provided string into an array based on the array of delimiters provided
    483:      *
    484:      * @param string $string The string to explode.
    485:      * @param array $delimiters An array of delimiters.
    486:      * @todo move to string utils class
    487:      * @return array
    488:      * @since 1.2
    489:      */
    490:     private static function multipleExplode($string, $delimiters = array()){
    491:
    492:         $mainDelim=$delimiters[count($delimiters)-1];
    493:
    494:         array_pop($delimiters);
    495:
    496:         foreach($delimiters as $delimiter) {
    497:             $string = str_replace($delimiter, $mainDelim, $string);
    498:         }
    499:
    500:         $result = explode($mainDelim, $string);
    501:
    502:         return $result;
    503:     }
    504:
    505:     /**
    506:      * Handles all of the rules for mod_rewrite style URL parsing
    507:      *
    508:      * @since 1.0
    509:      */
    510:     private function handleModRewriteRequests() {
    511:         self::$logger->debug('>>handleModRewriteRequests');
    512:         global $config;
    513:
    514:         // strip off the system URL from the request URL
    515:         $request = str_replace($config->get('app.url'), '', 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
    516:         self::$logger->debug('$request is ['.$request.']');
    517:         $params = self::multipleExplode($request, array('/','?','&'));
    518:         self::$logger->debug('$params are ['.var_export($params, true).']');
    519:
    520:         try {
    521:             // first param will always be the controller alias
    522:             if(empty($this->currentAlias) && !empty($params[0]))
    523:                 $this->currentAlias = $params[0];
    524:
    525:             // check to see if we can load the page controller without an alias
    526:             AlphaController::loadControllerDef($params[0]);
    527:             self::$logger->debug('Page controller name set on the request URL is ['.$params[0].']');
    528:             $this->pageController = $params[0];
    529:         }catch (IllegalArguementException $iae) {
    530:             // handle request with alias
    531:             self::$logger->debug('The supplied controller alias is ['.$this->currentAlias.']');
    532:
    533:             // check to see if the controller is an alias for something
    534:             if($this->checkAlias($this->currentAlias)) {
    535:                 $this->pageController = $this->getAliasController($this->currentAlias);
    536:                 self::$logger->debug('Page controller name obtained from the URL alias is ['.$this->pageController.']');
    537:
    538:                 if(isset($params[1])) {
    539:                     if(!empty($_POST))
    540:                         $_REQUEST[$this->getAliasParam($this->currentAlias)] = $params[1];
    541:                     else
    542:                         $_GET[$this->getAliasParam($this->currentAlias)] = $params[1];
    543:                 }
    544:             }
    545:         }
    546:
    547:         self::$logger->debug('$params are ['.var_export($params, true).']');
    548:         self::$logger->debug('currentAlias is ['.$this->currentAlias.']');
    549:
    550:         // now populate the _GET vars
    551:         if($this->currentAlias == 'tk') {
    552:             self::$logger->debug('Setting the GET vars for a mod_rewrite request with a tk param');
    553:             $this->setEncrypt(true);
    554:             $this->queryString = FrontController::decodeQueryParams($params[1]);
    555:             $_GET['tk'] = $params[1];
    556:             $this->populateGetVars();
    557:             $this->pageController = $_GET['act'];
    558:         }else{
    559:             $count = count($params);
    560:
    561:             for($i = 1; $i < $count; $i+=2) {
    562:                 if(isset($params[$i+1])) {
    563:                     if(!empty($_POST))
    564:                         $_REQUEST[$params[$i]] = $params[$i+1];
    565:                     else
    566:                         $_GET[$params[$i]] = $params[$i+1];
    567:                 }
    568:             }
    569:         }
    570:
    571:         self::$logger->debug('$_GET is ['.var_export($_GET, true).']');
    572:         self::$logger->debug('<<handleModRewriteRequests');
    573:     }
    574:
    575:     /**
    576:      * Getter for the page controller
    577:      *
    578:      * @return string
    579:      * @since 1.0
    580:      */
    581:     public function getPageController() {
    582:         return $this->pageController;
    583:     }
    584:
    585:     /**
    586:      * Add the supplied filter object to the list of filters ran on each request to the front controller
    587:      *
    588:      * @param AlphaFilterInterface $filterObject
    589:      * @throws IllegalArguementException
    590:      * @since 1.0
    591:      */
    592:     public function registerFilter($filterObject) {
    593:         if($filterObject instanceof AlphaFilterInterface)
    594:             array_push($this->filters, $filterObject);
    595:         else
    596:             throw new IllegalArguementException('Supplied filter object is not a valid AlphaFilterInterface instance!');
    597:     }
    598:
    599:     /**
    600:      * Returns the array of filters currently attached to this FrontController
    601:      *
    602:      * @return array
    603:      * @since 1.0
    604:      */
    605:     public function getFilters() {
    606:         return $this->filters;
    607:     }
    608: }
    609:
    610: ?>