Source for file dispatcher.php

Documentation is available at dispatcher.php

  1. <?php
  2. /**
  3.  * @package     FrameworkOnFramework
  4.  * @subpackage  dispatcher
  5.  * @copyright   Copyright (C) 2010 - 2012 Akeeba Ltd. All rights reserved.
  6.  * @license     GNU General Public License version 2 or later; see LICENSE.txt
  7.  */
  8. // Protect from unauthorized access
  9. defined('_JEXEC') or die;
  10.  
  11. /**
  12.  * FrameworkOnFramework dispatcher class
  13.  *
  14.  * FrameworkOnFramework is a set of classes whcih extend Joomla! 1.5 and later's
  15.  * MVC framework with features making maintaining complex software much easier,
  16.  * without tedious repetitive copying of the same code over and over again.
  17.  *
  18.  * @package  FrameworkOnFramework
  19.  * @since    1.0
  20.  */
  21. class FOFDispatcher extends JObject
  22. {
  23.     /** @var array Configuration variables */
  24.     protected $config = array();
  25.  
  26.     /** @var FOFInput Input variables */
  27.     protected $input = array();
  28.  
  29.     /** @var string The name of the default view, in case none is specified */
  30.     public $defaultView = 'cpanel';
  31.  
  32.     // Variables for FOF's transparent user authentication. You can override them
  33.     // in your Dispatcher's __construct() method.
  34.  
  35.     /** @var int The Time Step for the TOTP used in FOF's transparent user authentication */
  36.     protected $fofAuth_timeStep = 6;
  37.  
  38.     /** @var string The key for the TOTP, Base32 encoded (watch out; Base32, NOT Base64!) */
  39.     protected $fofAuth_Key = null;
  40.  
  41.     /** @var array Which formats to be handled by transparent authentication */
  42.     protected $fofAuth_Formats = array('json', 'csv', 'xml', 'raw');
  43.  
  44.     /**
  45.      * Should I logout the transparently authenticated user on logout?
  46.      * Recommended to leave it on in order to avoid crashing the sessions table.
  47.      *
  48.      * @var boolean 
  49.      */
  50.     protected $fofAuth_LogoutOnReturn = true;
  51.  
  52.     /** @var array Which methods to use to fetch authentication credentials and in which order */
  53.     protected $fofAuth_AuthMethods = array(
  54.         /* HTTP Basic Authentication using encrypted information protected
  55.          * with a TOTP (the username must be "_fof_auth") */
  56.         'HTTPBasicAuth_TOTP',
  57.         /* Encrypted information protected with a TOTP passed in the
  58.          * _fofauthentication query string parameter */
  59.         'QueryString_TOTP',
  60.         /* HTTP Basic Authentication using a username and password pair in plain text */
  61.         'HTTPBasicAuth_Plaintext',
  62.         /* Plaintext, JSON-encoded username and password pair passed in the
  63.          * _fofauthentication query string parameter */
  64.         'QueryString_Plaintext',
  65.         /* Plaintext username and password in the _fofauthentication_username
  66.          * and _fofauthentication_username query string parameters */
  67.         'SplitQueryString_Plaintext',
  68.     );
  69.  
  70.     /** @var bool Did we successfully and transparently logged in a user? */
  71.     private $_fofAuth_isLoggedIn = false;
  72.  
  73.     /** @var string The calculated encryption key for the _TOTP methods, used if we have to encrypt the reply */
  74.     private $_fofAuth_CryptoKey = '';
  75.  
  76.     /**
  77.      * Get a static (Singleton) instance of a particular Dispatcher
  78.      *
  79.      * @param   string  $option  The component name
  80.      * @param   string  $view    The View name
  81.      * @param   array   $config  Configuration data
  82.      *
  83.      * @staticvar  array  $instances  Holds the array of Dispatchers FOF knows about
  84.      *
  85.      * @return  FOFDispatcher 
  86.      */
  87.     public static function &getAnInstance($option = null, $view = null, $config = array())
  88.     {
  89.         static $instances = array();
  90.  
  91.         $hash = $option . $view;
  92.  
  93.         if (!array_key_exists($hash, $instances))
  94.         {
  95.             $instances[$hash] = self::getTmpInstance($option, $view, $config);
  96.         }
  97.  
  98.         return $instances[$hash];
  99.     }
  100.  
  101.     /**
  102.      * Gets a temporary instance of a Dispatcher
  103.      *
  104.      * @param   string  $option  The component name
  105.      * @param   string  $view    The View name
  106.      * @param   array   $config  Configuration data
  107.      *
  108.      * @return FOFDispatcher 
  109.      */
  110.     public static function &getTmpInstance($option = null, $view = null, $config = array())
  111.     {
  112.         if (array_key_exists('input', $config))
  113.         {
  114.             if ($config['input'] instanceof FOFInput)
  115.             {
  116.                 $input = $config['input'];
  117.             }
  118.             else
  119.             {
  120.                 if (!is_array($config['input']))
  121.                 {
  122.                     $config['input'] = (array) $config['input'];
  123.                 }
  124.  
  125.                 $config['input'] = array_merge($_REQUEST, $config['input']);
  126.                 $input = new FOFInput($config['input']);
  127.             }
  128.         }
  129.         else
  130.         {
  131.             $input = new FOFInput;
  132.         }
  133.  
  134.         $config['option'] = !is_null($option) ? $option : $input->getCmd('option', 'com_foobar');
  135.         $config['view'] = !is_null($view) ? $view : $input->getCmd('view', '');
  136.         $input->set('option', $config['option']);
  137.         $input->set('view', $config['view']);
  138.         $config['input'] = $input;
  139.  
  140.         $className = ucfirst(str_replace('com_', '', $config['option'])) . 'Dispatcher';
  141.  
  142.         if (!class_exists($className))
  143.         {
  144.             $componentPaths = FOFPlatform::getInstance()->getComponentBaseDirs($config['option']);
  145.  
  146.             $searchPaths = array(
  147.                 $componentPaths['main'],
  148.                 $componentPaths['main'] . '/dispatchers',
  149.                 $componentPaths['admin'],
  150.                 $componentPaths['admin'] . '/dispatchers'
  151.             );
  152.  
  153.             if (array_key_exists('searchpath', $config))
  154.             {
  155.                 array_unshift($searchPaths, $config['searchpath']);
  156.             }
  157.  
  158.             JLoader::import('joomla.filesystem.path');
  159.  
  160.             $path = JPath::find(
  161.                     $searchPaths, 'dispatcher.php'
  162.             );
  163.  
  164.             if ($path)
  165.             {
  166.                 require_once $path;
  167.             }
  168.         }
  169.  
  170.         if (!class_exists($className))
  171.         {
  172.             $className = 'FOFDispatcher';
  173.         }
  174.  
  175.         $instance = new $className($config);
  176.  
  177.         return $instance;
  178.     }
  179.  
  180.     /**
  181.      * Public constructor
  182.      *
  183.      * @param   array  $config  The configuration variables
  184.      */
  185.     public function __construct($config = array())
  186.     {
  187.         // Cache the config
  188.         $this->config = $config;
  189.  
  190.         // Get the input for this MVC triad
  191.         if (array_key_exists('input', $config))
  192.         {
  193.             $this->input = $config['input'];
  194.         }
  195.         else
  196.         {
  197.             $this->input = new FOFInput;
  198.         }
  199.  
  200.         // Get the default values for the component name
  201.         $this->component = $this->input->getCmd('option', 'com_foobar');
  202.  
  203.         // Load the component's fof.xml configuration file
  204.         $configProvider = new FOFConfigProvider;
  205.         $this->defaultView = $configProvider->get($this->component . '.dispatcher.default_view', $this->defaultView);
  206.  
  207.         // Get the default values for the view name
  208.         $this->view = $this->input->getCmd('view', null);
  209.  
  210.         if (empty($this->view))
  211.         {
  212.             // Do we have a task formatted as controller.task?
  213.             $task = $this->input->getCmd('task', '');
  214.  
  215.             if (!empty($task) && (strstr($task, '.') !== false))
  216.             {
  217.                 list($this->view, $task) = explode('.', $task, 2);
  218.                 $this->input->set('task', $task);
  219.             }
  220.         }
  221.  
  222.         if (empty($this->view))
  223.         {
  224.             $this->view = $this->defaultView;
  225.         }
  226.  
  227.         $this->layout = $this->input->getCmd('layout', null);
  228.  
  229.         // Overrides from the config
  230.         if (array_key_exists('option', $config))
  231.         {
  232.             $this->component = $config['option'];
  233.         }
  234.  
  235.         if (array_key_exists('view', $config))
  236.         {
  237.             $this->view = empty($config['view']) ? $this->view : $config['view'];
  238.         }
  239.  
  240.         if (array_key_exists('layout', $config))
  241.         {
  242.             $this->layout = $config['layout'];
  243.         }
  244.  
  245.         $this->input->set('option', $this->component);
  246.         $this->input->set('view', $this->view);
  247.         $this->input->set('layout', $this->layout);
  248.     }
  249.  
  250.     /**
  251.      * The main code of the Dispatcher. It spawns the necessary controller and
  252.      * runs it.
  253.      *
  254.      * @return  null|Exception
  255.      */
  256.     public function dispatch()
  257.     {
  258.         if (!FOFPlatform::getInstance()->authorizeAdmin($this->input->getCmd('option', 'com_foobar')))
  259.         {
  260.             if (FOFPlatform::getInstance()->checkVersion(JVERSION, '3.0', 'ge'))
  261.             {
  262.                 throw new Exception(JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
  263.             }
  264.             else
  265.             {
  266.                 return JError::raiseError('403', JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'));
  267.             }
  268.         }
  269.  
  270.         $this->transparentAuthentication();
  271.  
  272.         // Merge English and local translations
  273.         FOFPlatform::getInstance()->loadTranslations($this->component);
  274.  
  275.         $canDispatch = true;
  276.  
  277.         if (FOFPlatform::getInstance()->isCli())
  278.         {
  279.             $canDispatch = $canDispatch && $this->onBeforeDispatchCLI();
  280.         }
  281.  
  282.         $canDispatch = $canDispatch && $this->onBeforeDispatch();
  283.  
  284.         if (!$canDispatch)
  285.         {
  286.             JResponse::setHeader('Status', '403 Forbidden', true);
  287.  
  288.             if (FOFPlatform::getInstance()->checkVersion(JVERSION, '3.0', 'ge'))
  289.             {
  290.                 throw new Exception(JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
  291.             }
  292.             else
  293.             {
  294.                 return JError::raiseError('403', JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'));
  295.             }
  296.         }
  297.  
  298.         // Get and execute the controller
  299.         $option = $this->input->getCmd('option', 'com_foobar');
  300.         $view = $this->input->getCmd('view', $this->defaultView);
  301.         $task = $this->input->getCmd('task', null);
  302.  
  303.         if (empty($task))
  304.         {
  305.             $task = $this->getTask($view);
  306.         }
  307.  
  308.         // Pluralise/sungularise the view name for typical tasks
  309.  
  310.         if (in_array($task, array('edit', 'add', 'read')))
  311.         {
  312.             $view = FOFInflector::singularize($view);
  313.         }
  314.         elseif (in_array($task, array('browse')))
  315.         {
  316.             $view = FOFInflector::pluralize($view);
  317.         }
  318.  
  319.         $this->input->set('view', $view);
  320.         $this->input->set('task', $task);
  321.  
  322.         $config = $this->config;
  323.         $config['input'] = $this->input;
  324.  
  325.         $controller = FOFController::getTmpInstance($option, $view, $config);
  326.         $status = $controller->execute($task);
  327.  
  328.         if (!$this->onAfterDispatch())
  329.         {
  330.             JResponse::setHeader('Status', '403 Forbidden', true);
  331.  
  332.             if (FOFPlatform::getInstance()->checkVersion(JVERSION, '3.0', 'ge'))
  333.             {
  334.                 throw new Exception(JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
  335.             }
  336.             else
  337.             {
  338.                 return JError::raiseError('403', JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'));
  339.             }
  340.         }
  341.  
  342.         $format = $this->input->get('format', 'html', 'cmd');
  343.         $format = empty($format) ? 'html' : $format;
  344.  
  345.         if ($format == 'html')
  346.         {
  347.             // In HTML views perform a redirection
  348.             if ($controller->redirect())
  349.             {
  350.                 return;
  351.             }
  352.         }
  353.         else
  354.         {
  355.             // In non-HTML views just exit the application with the proper HTTP headers
  356.             if ($controller->hasRedirect())
  357.             {
  358.                 $headers = JResponse::sendHeaders();
  359.                 jexit();
  360.             }
  361.         }
  362.     }
  363.  
  364.     /**
  365.      * Tries to guess the controller task to execute based on the view name and
  366.      * the HTTP request method.
  367.      *
  368.      * @param   string  $view  The name of the view
  369.      *
  370.      * @return  string  The best guess of the task to execute
  371.      */
  372.     protected function getTask($view)
  373.     {
  374.         // Get a default task based on plural/singular view
  375.         $request_task = $this->input->getCmd('task', null);
  376.         $task = FOFInflector::isPlural($view) ? 'browse' : 'edit';
  377.  
  378.         // Get a potential ID, we might need it later
  379.         $id = $this->input->get('id', null, 'int');
  380.  
  381.         if ($id == 0)
  382.         {
  383.             $ids = $this->input->get('ids', array(), 'array');
  384.  
  385.             if (!empty($ids))
  386.             {
  387.                 $id = array_shift($ids);
  388.             }
  389.         }
  390.  
  391.         // Check the request method
  392.  
  393.         if (!isset($_SERVER['REQUEST_METHOD']))
  394.         {
  395.             $_SERVER['REQUEST_METHOD'] = 'GET';
  396.         }
  397.  
  398.         $requestMethod = strtoupper($_SERVER['REQUEST_METHOD']);
  399.  
  400.         switch ($requestMethod)
  401.         {
  402.             case 'POST':
  403.             case 'PUT':
  404.                 if (!is_null($id))
  405.                 {
  406.                     $task = 'save';
  407.                 }
  408.                 break;
  409.  
  410.             case 'DELETE':
  411.                 if ($id != 0)
  412.                 {
  413.                     $task = 'delete';
  414.                 }
  415.                 break;
  416.  
  417.             case 'GET':
  418.             default:
  419.                 // If it's an edit without an ID or ID=0, it's really an add
  420.                 if (($task == 'edit') && ($id == 0))
  421.                 {
  422.                     $task = 'add';
  423.                 }
  424.  
  425.                 // If it's an edit in the frontend, it's really a read
  426.                 elseif (($task == 'edit') && FOFPlatform::getInstance()->isFrontend())
  427.                 {
  428.                     $task = 'read';
  429.                 }
  430.                 break;
  431.         }
  432.  
  433.         return $task;
  434.     }
  435.  
  436.     /**
  437.      * Executes right before the dispatcher tries to instantiate and run the
  438.      * controller.
  439.      *
  440.      * @return  boolean  Return false to abort
  441.      */
  442.     public function onBeforeDispatch()
  443.     {
  444.         return true;
  445.     }
  446.  
  447.     /**
  448.      * Sets up some environment variables, so we can work as usually on CLI, too.
  449.      *
  450.      * @return  boolean  Return false to abort
  451.      */
  452.     public function onBeforeDispatchCLI()
  453.     {
  454.         JLoader::import('joomla.environment.uri');
  455.         JLoader::import('joomla.application.component.helper');
  456.  
  457.         // Trick to create a valid url used by JURI
  458.         $this->_originalPhpScript = '';
  459.  
  460.         // We have no Application Helper (there is no Application!), so I have to define these constants manually
  461.         $option = $this->input->get('option', '', 'cmd');
  462.  
  463.         if ($option)
  464.         {
  465.             $componentPaths = FOFPlatform::getInstance()->getComponentBaseDirs($option);
  466.  
  467.             if (!defined('JPATH_COMPONENT'))
  468.             {
  469.                 define('JPATH_COMPONENT', $componentPaths['main']);
  470.             }
  471.  
  472.             if (!defined('JPATH_COMPONENT_SITE'))
  473.             {
  474.                 define('JPATH_COMPONENT_SITE', $componentPaths['site']);
  475.             }
  476.  
  477.             if (!defined('JPATH_COMPONENT_ADMINISTRATOR'))
  478.             {
  479.                 define('JPATH_COMPONENT_ADMINISTRATOR', $componentPaths['admin']);
  480.             }
  481.         }
  482.  
  483.         return true;
  484.     }
  485.  
  486.     /**
  487.      * Executes right after the dispatcher runs the controller.
  488.      *
  489.      * @return  boolean  Return false to abort
  490.      */
  491.     public function onAfterDispatch()
  492.     {
  493.         // If we have to log out the user, please do so now
  494.  
  495.         if ($this->fofAuth_LogoutOnReturn && $this->_fofAuth_isLoggedIn)
  496.         {
  497.             FOFPlatform::getInstance()->logoutUser();
  498.         }
  499.  
  500.         return true;
  501.     }
  502.  
  503.     /**
  504.      * Transparently authenticates a user
  505.      *
  506.      * @return  void 
  507.      */
  508.     public function transparentAuthentication()
  509.     {
  510.         // Only run when there is no logged in user
  511.  
  512.         if (!FOFPlatform::getInstance()->getUser()->guest)
  513.         {
  514.             return;
  515.         }
  516.  
  517.         // @todo Check the format
  518.         $format = $this->input->getCmd('format', 'html');
  519.  
  520.         if (!in_array($format, $this->fofAuth_Formats))
  521.         {
  522.             return;
  523.         }
  524.  
  525.         foreach ($this->fofAuth_AuthMethods as $method)
  526.         {
  527.             // If we're already logged in, don't bother
  528.  
  529.             if ($this->_fofAuth_isLoggedIn)
  530.             {
  531.                 continue;
  532.             }
  533.  
  534.             // This will hold our authentication data array (username, password)
  535.             $authInfo = null;
  536.  
  537.             switch ($method)
  538.             {
  539.                 case 'HTTPBasicAuth_TOTP':
  540.  
  541.                     if (empty($this->fofAuth_Key))
  542.                     {
  543.                         continue;
  544.                     }
  545.  
  546.                     if (!isset($_SERVER['PHP_AUTH_USER']))
  547.                     {
  548.                         continue;
  549.                     }
  550.  
  551.                     if (!isset($_SERVER['PHP_AUTH_PW']))
  552.                     {
  553.                         continue;
  554.                     }
  555.  
  556.                     if ($_SERVER['PHP_AUTH_USER'] != '_fof_auth')
  557.                     {
  558.                         continue;
  559.                     }
  560.  
  561.                     $encryptedData = $_SERVER['PHP_AUTH_PW'];
  562.  
  563.                     $authInfo = $this->_decryptWithTOTP($encryptedData);
  564.                     break;
  565.  
  566.                 case 'QueryString_TOTP':
  567.                     $encryptedData = $this->input->get('_fofauthentication', '', 'raw');
  568.  
  569.                     if (empty($encryptedData))
  570.                     {
  571.                         continue;
  572.                     }
  573.  
  574.                     $authInfo = $this->_decryptWithTOTP($encryptedData);
  575.                     break;
  576.  
  577.                 case 'HTTPBasicAuth_Plaintext':
  578.                     if (!isset($_SERVER['PHP_AUTH_USER']))
  579.                     {
  580.                         continue;
  581.                     }
  582.  
  583.                     if (!isset($_SERVER['PHP_AUTH_PW']))
  584.                     {
  585.                         continue;
  586.                     }
  587.  
  588.                     $authInfo = array(
  589.                         'username'     => $_SERVER['PHP_AUTH_USER'],
  590.                         'password'     => $_SERVER['PHP_AUTH_PW']
  591.                     );
  592.                     break;
  593.  
  594.                 case 'QueryString_Plaintext':
  595.                     $jsonencoded = $this->input->get('_fofauthentication', '', 'raw');
  596.  
  597.                     if (empty($jsonencoded))
  598.                     {
  599.                         continue;
  600.                     }
  601.  
  602.                     $authInfo = json_decode($jsonencoded, true);
  603.  
  604.                     if (!is_array($authInfo))
  605.                     {
  606.                         $authInfo = null;
  607.                     }
  608.                     elseif (!array_key_exists('username', $authInfo) || !array_key_exists('password', $authInfo))
  609.                     {
  610.                         $authInfo = null;
  611.                     }
  612.                     break;
  613.  
  614.                 case 'SplitQueryString_Plaintext':
  615.                     $authInfo = array(
  616.                         'username'     => $this->input->get('_fofauthentication_username', '', 'raw'),
  617.                         'password'     => $this->input->get('_fofauthentication_password', '', 'raw'),
  618.                     );
  619.  
  620.                     if (empty($authInfo['username']))
  621.                     {
  622.                         $authInfo = null;
  623.                     }
  624.  
  625.                     break;
  626.  
  627.                 default:
  628.                     continue;
  629.  
  630.                     break;
  631.             }
  632.  
  633.             // No point trying unless we have a username and password
  634.             if (!is_array($authInfo))
  635.             {
  636.                 continue;
  637.             }
  638.  
  639.             $this->_fofAuth_isLoggedIn = FOFPlatform::getInstance()->loginUser($authInfo);
  640.         }
  641.     }
  642.  
  643.     /**
  644.      * Decrypts a transparent authentication message using a TOTP
  645.      *
  646.      * @param   string  $encryptedData  The encrypted data
  647.      *
  648.      * @return  array  The decrypted data
  649.      */
  650.     private function _decryptWithTOTP($encryptedData)
  651.     {
  652.         if (empty($this->fofAuth_Key))
  653.         {
  654.             $this->_fofAuth_CryptoKey = null;
  655.  
  656.             return null;
  657.         }
  658.  
  659.         $totp = new FOFEncryptTotp($this->fofAuth_timeStep);
  660.         $period = $totp->getPeriod();
  661.         $period--;
  662.  
  663.         for ($i = 0; $i <= 2; $i++)
  664.         {
  665.             $time = ($period + $i) * $this->fofAuth_timeStep;
  666.             $otp = $totp->getCode($this->fofAuth_Key, $time);
  667.             $this->_fofAuth_CryptoKey = hash('sha256', $this->fofAuth_Key . $otp);
  668.  
  669.             $aes = new FOFEncryptAes($this->_fofAuth_CryptoKey);
  670.             $ret = $aes->decryptString($encryptedData);
  671.             $ret = rtrim($ret, "\000");
  672.  
  673.             $ret = json_decode($ret, true);
  674.  
  675.             if (!is_array($ret))
  676.             {
  677.                 continue;
  678.             }
  679.  
  680.             if (!array_key_exists('username', $ret))
  681.             {
  682.                 continue;
  683.             }
  684.  
  685.             if (!array_key_exists('password', $ret))
  686.             {
  687.                 continue;
  688.             }
  689.  
  690.             // Successful decryption!
  691.             return $ret;
  692.         }
  693.  
  694.         // Obviously if we're here we could not decrypt anything. Bail out.
  695.         $this->_fofAuth_CryptoKey = null;
  696.  
  697.         return null;
  698.     }
  699.  
  700.     /**
  701.      * Creates a decryption key for use with the TOTP decryption method
  702.      *
  703.      * @param   integer  $time  The timestamp used for TOTP calculation, leave empty to use current timestamp
  704.      *
  705.      * @return  string  THe encryption key
  706.      */
  707.     private function _createDecryptionKey($time = null)
  708.     {
  709.         $totp = new FOFEncryptTotp($this->fofAuth_timeStep);
  710.         $otp = $totp->getCode($this->fofAuth_Key, $time);
  711.  
  712.         $key = hash('sha256', $this->fofAuth_Key . $otp);
  713.  
  714.         return $key;
  715.     }
  716.  
  717.     /**
  718.      * Main function to detect if we're running in a CLI environment and we're admin
  719.      *
  720.      * @return  array  isCLI and isAdmin. It's not an associtive array, so we can use list.
  721.      */
  722.     public static function isCliAdmin()
  723.     {
  724.         static $isCLI   = null;
  725.         static $isAdmin = null;
  726.  
  727.         if (is_null($isCLI) && is_null($isAdmin))
  728.         {
  729.             $isCLI   = FOFPlatform::getInstance()->isCli();
  730.             $isAdmin = FOFPlatform::getInstance()->isBackend();
  731.         }
  732.  
  733.         return array($isCLI, $isAdmin);
  734.     }
  735. }

Documentation generated on Tue, 19 Nov 2013 15:01:34 +0100 by phpDocumentor 1.4.3