<?php
/**
 * -------------------------------------------------------------------------
 *
 * The class for a web-based application.
 *
 * -------------------------------------------------------------------------
 *
 * Look at this diagram to understand how the app works:
 *
 *     ├─> .htaccess
 *     │
 *     │   ┌─<─ constant MIMIMI_APP_FOLDER
 *     │   ├─<─ constant ...
 *     │   │
 *     │   ├─<─ routine ...
 *     │   │
 *     └─> index.php
 *           │
 *           ├─> mimimi.core/Config.php
 *           │               │
 *           │               └─<─ constant ...
 *           │
 *           ├─> mimimi.core/RoutinesSystem.php
 *           │               │
 *           │               ├─<─ constant ...
 *           │               │
 *           │               ├─<─ routine mimimiInclude()
 *           │               ├─<─ routine mimimiServer()
 *           │               ├─<─ routine mimimiStop()
 *           │               └─<─ routine ...
 *           │                                      ┌─<─ mimimi.core/RoutinesWeb.php
 *           │   ┌───────────────────────────────┐  │                │
 *           ├─> │ MAKE THE $app GLOBAL VARIABLE │  │                ├─<─ constant ...
 *           │   └───────────────────────────────┘  │                │
 *           │                                      │                ├─<─ routine mimimiUri()
 *           │                                      │                └─<─ routine ...
 *           │                                      │
 *           │                                      │                        ┌─<─ class mimimi.core/Module.php
 *           │                                      │                        │                      │  └─> __construct()
 *           │               ┌──────────────────────┘                        │                      │
 *           │               │                                               │                      ├─<─ property $owner
 *           │               │                                               │                      ├─<─ property $app
 *           │               │                                               │                      │
 *           │               │                        ┌─<─ class mimimi.core/NodeModule.php         └─<─ method run()
 *           │               │                        │                      │    └─> __construct()*
 *           │               │ ┌─<─ class mimimi.core/Application.php        │
 *           │               │ │                          │                  ├─<─ property $myNodeFile
 *           └─> mimimi.core/Website.php                  └─> run()*         │
 *                           │   │                                           ├─<─ method __get()
 *                           │   └─> run()*                                  └─<─ method getNodePath()
 *  $pollerCollector prop ─>─┤        │
 *  $viewerCollector prop ─>─┤        ├─> admitVisitor()
 *  $systemCollector prop ─>─┤        │        │
 *   $allowedPollers prop ─>─┤        │        │   ┌───────────────────────────────┐
 *   $allowedViewers prop ─>─┤        │        │   │       IT CAN BE USED FOR      │
 *   $errorBadMethod prop ─>─┤        │        └─> │      FACE CONTROL TO STOP     │
 *    $errorNoModule prop ─>─┤        │            │  THE APPLICATION IMMEDIATELY  │
 *                           │        │            └───────────────────────────────┘
 *     runPoller() method ─>─┤        │
 *     runViewer() method ─>─┤        └─> routeUrl()
 *     runHelper() method ─>─┤              │
 *    callModule() method ─>─┤              ├─> cutUrlSegment()          ┌─<─ class mimimi.core/Module.php
 * cutUrlSegment() method ─>─┤              │        │                   │
 cutUrlPaginator() method ─>─┤              │        ├─> mimimi.core/Has/Has.php
 *   hasTemplate() method ─>─┤              │        │                   │                  ┌──────────────┐
 *renderTemplate() method ─>─┤              │        │                   └─<─ method __get( │ PROPERTY URL │ ) 
 *    renderPage() method ─>─┤              │        │                                      └──────────────┘
 *   systemError() method ─>─┤              │        │
 *                           │              │        │                                    ┌─<─ class mimimi.core/Module.php
 *  admitVisitor() method ─>─┤              │        │                                    │
 *      routeUrl() method ─>─┤              │        │                  ┌──────────────┐ ┌──────────────┐
 *     isAllowed() method ─>─┘              │        └─> mimimi.modules/│ PROPERTY URL │/│ PROPERTY URL │.php
 *                                          │                           └──────────────┘ └──────────────┘
 *                                          │                                             │      │
 *                             ┌────────────┴─────────────┐                               │      └─> cutSegment()
 *                             │  IF THE URL IS DETECTED  │                               │
 *                             │   AS A POLLING LOCATOR   │                               └─<─ method ...
 *                             └────────────┬─────────────┘
 *                                          │
 *                                          │
 *                                          │              ┌──────────────────┐
 *                                          ├─> runPoller( │ PROPERTY $POLLER │ )
 *                                          │        │     └──────────────────┘
 *                                          │        │
 *                                          │        │                   ┌─<─ class mimimi.core/Module.php
 *                                          │        │                   │
 *                                          │        ├─> mimimi.core/Has/Has.php
 *                                          │        │                   │                  ┌────────────────┐
 *                                          │        │                   └─<─ method __get( │ PROPERTY POLLS │ )
 *                                          │        │                                      └────────────────┘            ┌─<─ class mimimi.core/Module.php
 *                                          │        │                                                                    │
 *                                          │        │                                             ┌─<─ class mimimi.core/NodeModule.php
 *                                          │        │                                             │
 *                                          │        │                         ┌────────────────┐ ┌────────────────┐
 *                                          │        └─> [ MIMIMI_APP_FOLDER ]/│ PROPERTY POLLS │/│ PROPERTY POLLS │.php
 *                                          │                                  └────────────────┘ └────────────────┘      ┌─<─ class mimimi.core/Module.php
 *                                          │                                                         │                   │
 *                                          │                                                         ├─> mimimi.core/Has/Has.php
 *                                          │                                                         │                   │                  ┌──────────────────┐
 *                                          │                                                         │                   └─<─ method __get( │ PROPERTY $POLLER │ )
 *                                          │                                                         │                                      └──────────────────┘
 *                             ┌────────────┴─────────────┐                                           │                         ┌────────────────┐ ┌──────────────────┐ ┌──────────────────┐
 *                             │  IF THE URL IS DETECTED  │                                           └─> [ MIMIMI_APP_FOLDER ]/│ PROPERTY POLLS │/│ PROPERTY $POLLER │/│ PROPERTY $POLLER │.php
 *                             │       AS A WEB PAGE      │                                                                     └────────────────┘ └──────────────────┘ └──────────────────┘
 *                             └────────────┬─────────────┘                                                                                                                      │
 *                                          │                                                                                                                                    └─> run()*
 *                                          │                                                                                                                                         │
 *                                          │              ┌──────────────────┐                                                           ┌───────────────────────────────────────┐   │
 *                                          ├─> runViewer( │ PROPERTY $VIEWER │ )                                                         │ SEND A RESPONSE TO THE USER'S BROWSER │ <─┘
 *                                          │        │     └──────────────────┘                                                           └───────────────────────────────────────┘
 *                                          │        │
 *                                          │        │                   ┌─<─ class mimimi.core/Module.php
 *                                          │        │                   │
 *                                          │        ├─> mimimi.core/Has/Has.php
 *                                          │        │                   │                  ┌──────────────────┐
 *                                          │        │                   └─<─ method __get( │ PROPERTY VIEWERS │ )
 *                                          │        │                                      └──────────────────┘            ┌─<─ class mimimi.core/Module.php
 *                                          │        │                                                                      │
 *                                          │        │                                               ┌─<─ class mimimi.core/NodeModule.php
 *                                          │        │                                               │
 *                                          │        │                         ┌──────────────────┐ ┌──────────────────┐
 *                                          │        └─> [ MIMIMI_APP_FOLDER ]/│ PROPERTY VIEWERS │/│ PROPERTY VIEWERS │.php
 *                                          │                                  └──────────────────┘ └──────────────────┘      ┌─<─ class mimimi.core/Module.php
 *                                          │                                                             │                   │
 *                                          │                                                             ├─> mimimi.core/Has/Has.php
 *                                          │                                                             │                   │                  ┌──────────────────┐
 *                                          │                                                             │                   └─<─ method __get( │ PROPERTY $VIEWER │ )
 *                                          │                                                             │                                      └──────────────────┘
 *                                          │                                                             │                         ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
 *                             ┌────────────┴─────────────┐                                               └─> [ MIMIMI_APP_FOLDER ]/│ PROPERTY VIEWERS │/│ PROPERTY $VIEWER │/│ PROPERTY $VIEWER │.php
 *                             │     IF THE URL IS NOT    │                                                                         └──────────────────┘ └──────────────────┘ └──────────────────┘
 *                             │  RECOGNIZED OR INCORRECT │                                                                                                                          │
 *                             └────────────┬─────────────┘                                                                                                                          └─> run()*
 *                                          │                                                                                                                                             │
 *                                          │                                                                                                       ┌─────────────────────────────────┐   │
 *                                          │                                                                                                       │ SEND HTML TO THE USER'S BROWSER │ <─┘
 *                                          └─> systemError()                                                                                       └─────────────────────────────────┘
 *                                                   │
 *                                                   │   ┌──────────────────────┐
 *                                                   └─> │ STOP THE APPLICATION │
 *                                                       └──────────────────────┘
 *
 * The right arrows show the order in which application files are loaded and
 * their methods that are called when processing a request to this app. The
 * left arrows show the class from which the corresponding application file
 * is derived. They also show some public routines, some public methods, or
 * some constants that the corresponding file exposes to other application
 * modules. The method or property that has been overridden is marked with
 * an asterisk.
 *
 * -------------------------------------------------------------------------
 *
 * Implemented properties below are:
 *     PROTECTED  $pollerCollector  -->  to name a module that will collect poller modules
 *     PROTECTED  $viewerCollector  -->  to name a module that will collect viewer modules
 *     PROTECTED  $systemCollector  -->  to name a module that will collect system modules
 *     PROTECTED  $allowedPollers   -->  to define valid names of poller modules
 *     PROTECTED  $allowedViewers   -->  to define valid names of poller modules
 *     PROTECTED  $errorBadMethod   -->  to define an error message 1
 *     PROTECTED  $errorNoModule    -->  to define an error message 2
 *
 * Overridden methods below are:
 *     run                     (                              $params )  -->  to process a browser request
 *
 * Implemented methods below are:
 *     cutUrlSegment           (                  $url                )  -->  to cut the first segment of URL
 *     cutUrlPaginator         (                  $url                )  -->  to cut the last segment of URL if it is pagination
 *     systemError             ( $message, $name                      )  -->  to stop working
 *     runPoller               (           $name, $url                )  -->  to launch a poller module
 *     runViewer               (           $name, $url                )  -->  to launch a viewer module
 *     runHelper               (                              $params )  -->  to launch the theme helper
 *     callModule              ( $node,    $name, $method, ...$params )  -->  to call a module method with parameters
 *     hasTemplate             ( $template                            )  -->  to check if a template file exists
 *     renderTemplate          ( $template, $error, $data             )  -->  to render a content using a template file
 *     renderPage              ( $template,         $data             )  -->  to render a page using a template file
 *     PROTECTED  admitVisitor (                                      )  -->  to block unwanted visitors
 *     PROTECTED  routeUrl     (                                      )  -->  to route the requested URL
 *     PROTECTED  isAllowed    (           $name, $list               )  -->  to check if a name exists in the list
 *
 * -------------------------------------------------------------------------
 *
 * @package    MimimiFramework
 * @subpackage Core
 * @copyright  2022 MiMiMi Community
 *             https://mimimi.software/
 * @license    GPL-2.0
 *             https://opensource.org/license/gpl-2-0/
 *
 * -------------------------------------------------------------------------
 */

    mimimiInclude ( 'Application.php' );
    mimimiLoad    ( 'RoutinesWeb.php' );

    class MimimiWebsite extends MimimiApplication {

        /**
         * -----------------------------------------------------------------
         *
         * Define module names that will collect all related modules in the
         * collector's folder. Typically, modules are located in the root
         * folder of your application. Such a single-level arrangement of
         * modules is convenient only when creating a simple application.
         * In more complex cases, it is better to move modules to special
         * folders that will serve the purpose of collecting all modules
         * of the same type in one place. This migration is purely for
         * organizing your app's file structure, not for improving code
         * efficiency. Leave these names blank if you want to revert to
         * a single-level module organization.
         *
         * -----------------------------------------------------------------
         *
         * @var    string
         * @access protected
         *
         * -----------------------------------------------------------------
         */

        protected $pollerCollector = 'polls';
        protected $viewerCollector = 'viewers';
        protected $systemCollector = 'system';

        /**
         * -----------------------------------------------------------------
         *
         * Define comma-separated names of visual modules that can be
         * referenced by URL. For example:
         *
         *      $allowedViewers = 'home, error404, robots_txt'
         *
         * It can also contain a wildcard * to refer to any module.
         *
         * -----------------------------------------------------------------
         *
         * @var    string
         * @access protected
         *
         * -----------------------------------------------------------------
         */

        protected $allowedPollers = '*';
        protected $allowedViewers = '*';

        /**
         * -----------------------------------------------------------------
         *
         * Define a few system error messages. Please note that the # symbol
         * is a placeholder for the name of the error subject.
         *
         * -----------------------------------------------------------------
         *
         * @var    string
         * @access protected
         *
         * -----------------------------------------------------------------
         */

        protected $errorBadMethod = 'Method # is not allowed. Only methods like HEAD, GET, and POST are allowed when accessing this site.';
        protected $errorNoModule  = 'The site does not have the necessary # visual module, nor does it have the Error404 module!';

        /**
         * -----------------------------------------------------------------
         *
         * Starts processing the user's browser request.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   mixed  $params  (optional) It's a dummy that isn't really used here.
         * @return  void
         *
         * -----------------------------------------------------------------
         */

        public function run ( $params = '' ) {
            $method = mimimiServer ( 'REQUEST_METHOD' );
            switch ( $method ) {
                case 'HEAD':
                case 'GET':
                case 'POST':
                     $this->admitVisitor ( );
                     $this->routeUrl     ( );
                     break;
                default:
                    $this->systemError ( $this->errorBadMethod, $method );
            }
        }

        /**
         * -----------------------------------------------------------------
         *
         * Restricts access to the site based on visitor's history.
         *
         * -----------------------------------------------------------------
         *
         * @protected
         * @return  void
         *
         * -----------------------------------------------------------------
         */

        protected function admitVisitor ( ) {
        }

        /**
         * -----------------------------------------------------------------
         *
         * Routes the page URL requested by the user's browser.
         *
         * -----------------------------------------------------------------
         *
         * This method analyzes the requested URL to launch a viewer module
         * or a poller module corresponding to the URL. Only the following
         * locators will be processed by this method:
         *
         *     https://your.site/
         *
         *     https://your.site/robots.txt
         *     https://your.site/robots.txt/SOME-URL-PATH
         *
         *     https://your.site/A-VIEWER-NAME
         *     https://your.site/A-VIEWER-NAME/SOME-URL-PATH
         *
         *     https://your.site/polls/A-POLLER-NAME
         *     https://your.site/polls/A-POLLER-NAME/SOME-URL-PATH
         *
         * And these locators will be forcibly routed to the Error404 viewer
         * module:
         *
         *     https://your.site/robots_txt
         *     https://your.site/robots_txt/SOME-URL-PATH
         *
         *     https://your.site/error404
         *     https://your.site/error404/SOME-URL-PATH
         *
         *     https://your.site/polls
         *
         * If the app is missing a required viewer/poller module and also
         * a viewer module Error404, you will see a system error message
         * with the name of the missing module.
         *
         * -----------------------------------------------------------------
         *
         * @protected
         * @return  void
         *
         * -----------------------------------------------------------------
         */

        protected function routeUrl ( ) {
            $badChars = '~\W+~u';
            $url      = mimimiUri            ( FALSE         );
            $suburl   = mb_strtolower        ( $url, 'UTF-8' );
            $viewer   = $this->cutUrlSegment ( $suburl       );
            $poller   = '';
            switch ( $viewer ) {
                case '':
                     $viewer = 'home';
                     break;
                case 'robots.txt':
                     $viewer = 'robots_txt';
                     break;
                case 'robots_txt':
                case 'error404':
                     $viewer = '';
                     break;
                case 'polls':
                     $viewer = '';
                     $poller = $this->cutUrlSegment ( $suburl                 );
                     $poller = preg_replace         ( $badChars, '_', $poller );
                     break;
                default:
                     $viewer = preg_replace         ( $badChars, '_', $viewer );
            }
            $this          ->runPoller   ( $poller,              $suburl )
            || $this       ->runViewer   ( $viewer,              $suburl )
               || $this    ->runViewer   ( 'error404',           $url    )
                   || $this->systemError ( $this->errorNoModule, $viewer );
        }

        /**
         * -----------------------------------------------------------------
         *
         * Cuts out the first segment from the URL.
         *
         * -----------------------------------------------------------------
         *
         * Note that the input parameter is passed by reference and will
         * undergo the following changes:
         *
         *     if the incoming URL was like  Hello/My/Dear/Kitty
         *     the outgoing URL will become  My/Dear/Kitty
         *     and the method will return    hello
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   string  $url  A relative URL extracted from the browser request.
         * @return  string        The first segment of the incoming URL that was stripped as a parameter. Always in lowercase.
         *
         * -----------------------------------------------------------------
         */

        public function cutUrlSegment ( & $url ) {
            return $this->callModule ( $this->systemCollector, 'url', 'cutSegment', $url );
        }

        /**
         * -----------------------------------------------------------------
         *
         * Cuts out the pagination segment from the URL.
         *
         * -----------------------------------------------------------------
         *
         * Note that the input parameter is passed by reference and will
         * undergo the following changes:
         *
         *     if the incoming URL was like  Hello/My/Dear/Kitty/page-5
         *     the outgoing URL will become  Hello/My/Dear/Kitty
         *     and the method will return    5
         *
         *     if the incoming URL was like  Hello/My/Dear/Kitty
         *     the outgoing URL will become  Hello/My/Dear/Kitty
         *     and the method will return    1
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   string  $url  A relative URL extracted from the browser request.
         * @return  int           The page number that was stripped from the incoming URL.
         *
         * -----------------------------------------------------------------
         */

        public function cutUrlPaginator ( & $url ) {
            return $this->callModule ( $this->systemCollector, 'url', 'cutPaginator', $url );
        }

        /**
         * -----------------------------------------------------------------
         *
         * Launches the poller module.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   string  $name  The name of the poller module that will be launched.
         * @param   string  $url   (optional) The URL path that was found in the locator immediately after the poller name segment.
         * @return  bool           TRUE  if the poller was launched successfully.
         *                         FALSE if did not output anything and an Error404 page needs to be displayed.
         *                         NULL  if the poller is missing.
         *
         * -----------------------------------------------------------------
         */

        public function runPoller ( $name, $url = '' ) {
            return $this->isAllowed  ( $name, $this->allowedPollers )
                && $this->callModule ( $this->pollerCollector, $name, 'run', $url );
        }

        /**
         * -----------------------------------------------------------------
         *
         * Launches the viewer module.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   string  $name  The name of the viewer module that will be launched.
         * @param   string  $url   (optional) The URL path that was found in the locator immediately after the viewer name segment.
         * @return  bool           TRUE  if the viewer was launched successfully.
         *                         FALSE if did not output anything and an Error404 page needs to be displayed.
         *                         NULL  if the viewer is missing.
         *
         * -----------------------------------------------------------------
         */

        public function runViewer ( $name, $url = '' ) {
            return $this->isAllowed  ( $name, $this->allowedViewers )
                && $this->callModule ( $this->viewerCollector, $name, 'run', $url );
        }

        /**
         * -----------------------------------------------------------------
         *
         * Launches the Helper module.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   mixed  $params  (optional) Some parameters from the calling process.
         * @return  bool            TRUE  if the module was launched successfully.
         *                          FALSE if the module does not exist or is already running.
         *
         * -----------------------------------------------------------------
         */

        public function runHelper ( $params = '' ) {
            return $this->callModule ( $this->systemCollector, 'helper', 'run', $params );
        }

        /**
         * -----------------------------------------------------------------
         *
         * Calls the module method.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   string  $node    The name of the collector module that contains calling module.
         * @param   string  $name    The name of the module that will be called.
         * @param   string  $method  The name of the module method that will be called.
         * @param   mixed   $params  (optional) Some parameters for the method.
         * @return  mixed            RESULT returned by method if it was called successfully.
         *                           NULL   if the module is missing.
         *
         * -----------------------------------------------------------------
         */

        public function callModule ( $node, $name, $method, & ...$params ) {
            return $name   != ''
                && $method != ''
                   ?       $node != ''
                           ?     (  $this->has->$node
                                 && $this     ->$node->has->$name
                                    ?    $this->$node     ->$name->$method ( ...$params )
                                    :    NULL  )
                           :     (  $this            ->has->$name
                                    ?    $this            ->$name->$method ( ...$params )
                                    :    NULL  )
                   :       NULL;
        }

        /**
         * -----------------------------------------------------------------
         *
         * Checks if this module name is present in a comma-separated list.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   string  $name  The name of the module that will be checked.
         * @param   string  $list  Names of allowed modules, separated by commas. It can contain a wildcard * to allow any module.
         * @return  bool
         *
         * -----------------------------------------------------------------
         */

        protected function isAllowed ( $name, $list ) {
            return $name != ''
                && ( $list = preg_split ( '~\s*,[\s,]*~u', $list ) )
                && ( in_array ( '*',   $list )
                  || in_array ( $name, $list ) );
        }

        /**
         * -----------------------------------------------------------------
         *
         * Reports an app's system error and stops working.
         *
         * -----------------------------------------------------------------
         *
         * Please note that the error message string may contain # symbol.
         * It is a placeholder for the name of the error subject.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   string  $message  The error message string.
         * @param   string  $name     (optional) The name of the error subject.
         * @return  void
         *
         * -----------------------------------------------------------------
         */

        public function systemError ( $message, $name = '' ) {
            $name    = htmlspecialchars ( $name,   ENT_QUOTES, 'UTF-8'  );
            $name    = preg_replace     ( '~_+~u', ' ',        $name    );
            $name    = ucwords          ( $name                         );
            $name    = preg_replace     ( '~\s~u', '',         $name    );
            $message = preg_replace     ( '~\#~u', $name,      $message );
            mimimiStop ( $message );
        }

        /**
         * -----------------------------------------------------------------
         *
         * Checks if the theme template file exists.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   string  $template  The name of the template file to check.
         * @return  bool
         *
         * -----------------------------------------------------------------
         */

        public function hasTemplate ( $template ) {
            $root    = mimimiBasePath ( );
            $folder  = $this->getNodePath ( ) ;
            $storage = 'Themes'             . '/';
            $theme   = MIMIMI_APP_THEME     . '/';
            return file_exists ( $root . $folder . $storage . $theme . $template );
        }

        /**
         * -----------------------------------------------------------------
         *
         * Renders the theme template file.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   string  $template  The name of the template file that will be rendered.
         * @param   string  $error     (optional) "Error 404" message if that template is not found.
         * @param   mixed   $data      (optional) Data to be sent to the template.
         * @return  bool               Always TRUE.
         *
         * ---------------------------------------------------------------------
         */

        public function renderTemplate ( $template, $error = '', $data = FALSE ) {
            $this->hasTemplate   ( $template        )
            && $this->renderPage ( $template, $data )
               || !  empty      ( $error )
                  && mimimiStop ( $error );
            return TRUE;
        }

        /**
         * -----------------------------------------------------------------
         *
         * Renders the contents of a page using its theme template file.
         *
         * -----------------------------------------------------------------
         *
         * @public
         * @param   string  $template  The name of the template file that will be rendered.
         * @param   mixed   $data      (optional) Data to be sent to the template.
         * @return  bool               Always TRUE.
         *
         * -----------------------------------------------------------------
         */

        public function renderPage ( $template, $data = FALSE ) {
            $this->runHelper ( );
            mimimiModule (
                [ $template, $data ]
            );
            return TRUE;
        }
    };
