The Framework Code

class/framework/support/contextbase.php

File List

<?php
/**
 * Contains the definition of the Context class
 *
 * @author Lindsay Marshall <lindsay.marshall@ncl.ac.uk>
 * @copyright 2012-2021 Newcastle University
 * @package Framework\Framework\Support
 */
    namespace Framework\Support;

    use \Config\Config;
    use \Config\Framework as FW;
    use \RedBeanPHP\OODBBean;
/**
 * A class that stores various useful pieces of data for access throughout the rest of the system.
 *
 * This is a base class for the derived class \Framework\Context which adds
 * more features. This exists simply to reduce the overall complexity/density of the code rather
 * than to provide any other possibilities.
 *
 * Data that is local to the installation should be handled by the \Framework\Local class.
 */
    class ContextBase
    {
        use \Framework\Utility\Singleton;

/** @var ?OODBBean NULL or an object decribing the current logged in User (if we have logins at all) */
        protected ?OODBBean $luser        = NULL;
/** @var string The first component of the current URL */
        protected string $reqaction    = 'home';
/** @var array<string> The rest of the current URL exploded at / */
        protected array $reqrest      = [''];
/** @var bool True if authenticated by JWT token */
        protected bool $tokenAuth    = FALSE;
/** @var array<OODBBean>            A cache for rolename beans */
        protected array $roles        = [];
/** @var array<OODBBean>            A cache for rolecontext beans */
        protected array $contexts     = [];
/** @var array<array<string>>                   A cache for JS ons */
        protected array $ons          = [];
/** @var array<\Framework\FormData\Base>        FormData handler cache */
        protected array $getters      = [];
/*
 ***************************************
 * URL and REST support functions
 ***************************************
 */
/**
 * Return the main action part of the URL as set by .htaccess
 *
 * The framework trats a URL as /action/rest... The action returned is always in lowercase.
 */
        public function action() : string
        {
            return $this->reqaction;
        }
/**
 * Return the part of the URL after the main action as set by .htaccess
 *
 * See setup() below for how the URL is processed to create the result array.
 *
 * Note that if there is nothing after the action in the URL this function returns
 * an array with a single element containing an empty string.
 *
 * @return array<string>
 */
        public function rest() : array
        {
            return $this->reqrest;
        }
/**
 ***************************************
 * User related functions
 ***************************************
 */
/**
 * Return the current logged in user bean, if any
 */
        public function user() : ?OODBBean
        {
            return $this->luser;
        }
/**
 * Do we have a logged in user?
 */
        public function hasUser() : bool
        {
            return is_object($this->luser);
        }
/**
 * Find out if this was validated using a JWT token, if so, it is (probably) coming from a device not a browser
 */
        public function hasToken() : bool
        {
            return $this->tokenAuth;
        }
/*
 ***************************************
 * Miscellaneous utility functions
 ***************************************
 */
/**
 * Save values into the on cache
 *
 * ON JavaScript on tags is saved up so that it can all be generated into a single block
 * that can be hashed/nonced so that CSP does not complain.
 *
 * @param $id The id for the tag
 * @param $on The type of on (e.g. click, load etc.)
 * @param $fn The code to be executed
 *
 * @psalm-suppress PossiblyUnusedMethod
 */
        public function saveOn(string $id, string $on, string $fn) : void
        {
            $this->ons[$id][$on] = $fn;
        }
/**
 * Get the JS for onloading the ons
 *
 * This generates a block of vanilla JavaScript that will set up all the necessary in conditions.
 *
 * @psalm-suppress PossiblyUnusedMethod
 * @phpcsSuppress PhpCs.StringNotation.SingleQuoteFixer
 */
        public function getOns() : string
        {
            $res = '';
            foreach ($this->ons as $id => $conds)
            {
                foreach ($conds as $on => $fn)
                {
                    $res .= "document.getElementById('".$id."').addEventListener('".$on."', ".$fn.');'.PHP_EOL;
                }
            }
            return $res;
        }
/**
 * Find a rolename bean
 *
 * This will load the name cache if needed and then return the relevant bean (if it exists). The
 * existence test could be replaced by an assert if you really wanted.
 *
 * @param  $name   A Role name
 *
 * @throws \Framework\Exception\InternalError
 * @psalm-suppress PossiblyUnusedMethod
 */
        public function roleName(string $name) : OODBBean
        {
            if (!isset($this->roles[$name]))
            {
                if (!is_object($bn = \R::findOne(FW::ROLENAME, 'name=?', [$name])))
                {
                    throw new \Framework\Exception\InternalError('Missing role name: '.$name);
                }
                $this->roles[$name] = $bn;
            }
            return $this->roles[$name];
        }
/**
 * Find a rolecontext bean
 *
 * This will load the name cache if needed and then return the relevant bean (if it exists). The
 * existence test could be replaced by an assert if you really wanted.
 *
 * @param $name   A Context name
 *
 * @throws \Framework\Exception\InternalError
 * @psalm-suppress PossiblyUnusedMethod
 */
        public function roleContext(string $name) : OODBBean
        {
            if (!isset($this->contexts[$name]))
            {
                if (!is_object($bn = \R::findOne(FW::ROLECONTEXT, 'name=?', [$name])))
                {
                    throw new \Framework\Exception\InternalError('Missing context name: '.$name);
                }
                $this->contexts[$name] = $bn;
            }
            return $this->contexts[$name];
        }
/**
 * Load a bean that must exist, otherwise throw an exception.
 *
 * R::load returns a new bean with id 0 if the given id does not exist. This function throws an exception
 * if that happens as it is assumed that the bean must exist.
 *
 * @param $bean       A bean type name
 * @param $id         A bean id
 * @param $forupdate  If TRUE then use loadforupdate
 * @param $msg        A custom error message
 *
 * @throws  \Framework\Exception\MissingBean
 */
        public function load(string $bean, int $id, bool $forupdate = FALSE, string $msg = '') : OODBBean
        {
            $foo = $forupdate ? \R::loadForUpdate($bean, $id) : \R::load($bean, $id);
            if ($foo->getID() == 0)
            {
                throw new \Framework\Exception\MissingBean($msg !== '' ? $msg : 'Missing '.$bean);
            }
            return $foo;
        }
/**
 * Return the Local singleton
 *
 * @psalm-suppress MoreSpecificReturnType
 * @psalm-suppress LessSpecificReturnStatement
 * @psalm-suppress MoreSpecificReturnType
 */
        public function local() : \Framework\Local
        {
            return \Framework\Local::getInstance(); // @phan-suppress-current-line PhanTypeMismatchReturn
        }
/**
 * Return a Formdata object
 *
 * @param $which The formdata object needed - get, post, put, file, cookie
 *
 * @psalm-suppress LessSpecificReturnStatement
 * @psalm-suppress MoreSpecificReturnType
 */
        public function formData(string $which) : \Framework\FormData\Base
        {
            $which = strtolower($which);
            if (!isset($this->getters[$which]))
            {
                $class = '\Framework\FormData\\'.ucfirst($which);
                $this->getters[$which] = new $class();
            }
            return $this->getters[$which];
        }
/**
 * Return the Web singleton
 *
 * @psalm-suppress LessSpecificReturnStatement
 * @psalm-suppress MoreSpecificReturnType
 */
        public function web() : \Framework\Web\Web
        {
            return \Framework\Web\Web::getInstance(); // @phan-suppress-current-line PhanTypeMismatchReturn
        }
/*
 ***************************************
 * Setup the Context - the constructor is hidden in Singleton
 ***************************************
 */
/**
 * Look for a mobile access JWT token
 *
 * @internal
 *
 * @throws \Framework\Exception\InternalError
 */
        private function mtoken() : void
        {
            // This has to be a loop as we have no guarantees of the case of the keys in the returned array.
            $auth = \array_filter(\getallheaders(), static fn($key) => FW::AUTHTOKEN === \strtoupper($key), \ARRAY_FILTER_USE_KEY);
            if (!empty($auth))
            { // we have mobile authentication in use
                try
                {
                    /** @psalm-suppress UndefinedClass - the JWT code is not included in the psalm tests at the moment */
                    $tok = \Framework\Utility\JWT\JWT::decode(\array_shift($auth), FW::AUTHKEY);
                }
                catch (\Throwable $e)
                { // token error of some kind so return no access.
                    $this->web()->noaccess($e->getMessage());
                    /* NOT REACHED */
                }
                if (is_object($this->luser))
                {
                    if ($this->luser->getID() != $tok->sub)
                    {
                        throw new \Framework\Exception\InternalError('User conflict');
                    }
                }
                else
                {
                    $this->luser = $this->load(FW::USER, $tok->sub);
                }
                $this->tokenAuth = TRUE;
            }
        }
/**
 * Initialise the context and return self
 */
        public function setup() : ContextBase
        {
            \ini_set('session.use_only_cookies', '1'); // make sure PHP is set to make sessions use cookies only
            \ini_set('session.use_trans_sid', '0');   // this helps a bit towards making session hijacking more difficult
            \ini_set('session.cookie_httponly', '1');     // You can get rid of these calls if you know your php.ini is set up correctly
            if (isset($_COOKIE[Config::SESSIONNAME]))
            { // see if there is a userID variable in the session....
                /** @psalm-suppress UnusedFunctionCall */
                \session_start(['name' => Config::SESSIONNAME]);
                if (isset($_SESSION['userID']))
                { // there is a user id in the session so load the relevant user bean
                    $this->luser =  $this->load(FW::USER, $_SESSION['userID']);
                }
                else
                { // something not right so kill session and the session cookie
                    \session_destroy();
                    $params = \session_get_cookie_params();
                    \setcookie(\session_name(), '', \time() - 42000,
                        $params['path'], $params['domain'], $params['secure'], $params['httponly']
                    );
                }
            }
            $this->mtoken();

            $req = \array_filter(\explode('/', $this->web()->request()), static fn($val) => $val !== ''); // array_filter removes empty elements - trailing / or multiple /
/*
 * If you know that the base directory is empty then you can delete the next test block.
 *
 * You can also optimise out the loop if you know how deep you are nested in sub-directories
 *
 * The code here is to make it easier to move your code around within the hierarchy. If you don't need
 * this then optimise the hell out of it.
 */
            if ($this->local()->base() !== '')
            { // we are in at least one sub-directory
                $bsplit = \array_filter(\explode('/', $this->local()->base()), static fn($val) => $val !== '');
                $req = \array_slice($req, \count($bsplit));
            }
            if (!empty($req))
            { // there was something after the domain name so split it into action and rest...
                $this->reqaction = \strtolower(\array_shift($req));
                $this->reqrest = empty($req) ? [''] : $req;  // there may only have been an action
            }
            return $this;
        }
    }
?>