» /rd/lib.framework/Controller/Controller.php

<?php

/*

Actions:

    Action is considered as AJAX when it starts with
          "a." || "a_" || "a-" - NON CACHEBLE AJAX
          "c." || "c_" || "c-" - CACHEBLE AJAX

    Action is considered as internal when it starts with _

    Common for all Actions

        return value "a:xxx"
            - internal redirect to action "xxx"

        return value "r:template"
            - render template using render farm

        return value:
            - use template( "value" )

        return 0
            - stop processing, no template used

        return [ url, parameters ]
            - internal redirect to new url with new parameters

    Normal Actions:

        return NULL:
            - use template( "ClassName/methodname" ) (same name template)

     Ajax Actions:
        return NULL:
            - stop processing, no template used

Template name:
     Template name is the same as in URL (plus .phtml extension)


PROXING:

  Allows you to proxy actions and templates to alternative web root

  Support 2 methods -
     FULL-AUTO Action proxing and Manual Proxing

  Parameters:
    C('web-root-proxy')     - web root for proxy requests     REQUIRED
    C('action-proxy')       - AUTO proxy ALL actions          OPTIONAL
    C('template-proxy')     - AUTO proxy ALL templates        OPTIONAL (action-proxy implies template-proxy)

  FULL Action proxing:
    just setup C('action-proxy') or C('template-proxy')

  MANUAL Action proxing:

    Specific Action+Template Proxy : proxy ALL
    *  Create empty action file

    Specific method proxy:
    *  "return "p:method" -  proxy to another action/template in web-root-proxy (same parameters apply)
    *  attention: make sure u are using "Action"."_" class name when proxing method to same path

    Overriding methods: (php inheritance)
    *  Define class "ActionName"."_" inside action
       when '_' class found - template proxying is enabled
    *  if u wanna extend default action do: class Action_XX_ extends Action_XX { ... }


  Manual Template Proxing:
    *  Just do C_set('template-proxy',1) whenever u need it

LAYOUT CONTROL:
   no_layout=:    << empty value
       suppress all layouts

   no_layout=1:
       suppress extra layouts, header/title/etc is still shown

   FROM ACTION:
         Override layout by modifying Controller::$layout

DEBUG:
   $_GET["TPL"]:
       show template parameters and block calls inside of HTML
   $_GET["PHPINFO"]:
       show phpinfo

IDEAS:

  Move Logic out of Action Class
  All logic should be in Dispatcher
  Common naming


*/

class Controller
{


    
/*
     if (!Debug::is_admin() ) {
     $level = ob_get_level();
     register_shutdown_function(create_function('',"while(ob_get_level()>$level)ob_clean();"));
     }
    */

    
static $layout;

    private static 
$getData = [];
    private static 
$postData = [];
    private static 
$priorityByDirectory = [
//        ['dir' => '', 'priority' => 1,],
        
['dir' => 'Custom/''classPrefix' => 'Custom_''priority' => 10,]
    ];

    
/**
     * @param array $getData
     * @param array $postData
     * @return void
     */
    
public static function populateData(array $getData, array $postData): void
    
{
        
self::$getData self::sanitizeData($getData);
        
self::$postData self::sanitizeData($postData);
    }

    
/**
     * @param array $data
     * @return array
     */
    
public static function sanitizeData(array $data): array
    {
        
$badKey array_fill_keys(
            [
'_''__''CACHE'],
            
1
        
);
        return 
array_diff_key($data$badKey);
    }

    static public function 
dispatch($url, array $params=[], $layout="") { # void

        
if (Debug::is_admin() && class_exists("Grant")) {
            if (
Grant::grantIsActive(id(), \RDSite::siteId())) {
                
$master_info Grant::getMasterInfo(id(), \RDSite::siteId());
                
CD::set("GRANT"$master_info);
            }
        }

        
// Block forbidden parameters to be passed from outside
        
foreach (["_""__""CACHE"] as $fbdn) {
            if (isset(
$params[$fbdn])) {
                unset(
$params[$fbdn]);
            }
        }

        
self::$layout=$layout;

        if (
$r strpos($url,"?"))
            
$url substr($url0$r);
        
$url substr($url,1);    // remove leading "/"

        
restore_msg($params);

        
$template self::action($url$params); // PARAM by REF!

        
if (isset($_GET['TPL']) ) {
            echo 
"<div style='padding-left: 3px; background: #eee'>";
            
vvv($params);
            echo 
"</div>";
        }

        if (
array_key_exists("no_layout"$params) ) {
            if (
$params["no_layout"])
                
Template::$TPL->no_layout=1;
            else
                
self::$layout="";
        }

        
// IMPORTANT! This part is being used only if project uses "template" mode. For "view" mode we should not appear here.
        
if (self::$layout ) {
            
Profiler::in("Template:$template");

            $body fetch($template$params);

            
Profiler::out();

            
Profiler::in("Layout"$layout);
            
show($layout, ['body' => $body]);
            
Profiler::out();

            return;
        }

        
//Profiler::in("Template:$template");
        
show($template$params);
        
//Profiler::out();

    
}

    
// Cli mode shortcut
    // resolve and execute action
    // example: Controller::A("/srv/a.state_complete", ["q" => "M"])
    // example: Controller::A("/srv/a.state_complete?q=M"])
    
static function A($path, array $params=[]) { # result
        
if (strpos($path'?')) {
            
$path_query parse_url($path);
            
$path $path_query["path"];
            
$_params = [];
            
parse_str($path_query["query"], $_params);
            
$params $params $_params;
        }
        [
$class$method] = self::handler($path);
        if (! 
$class\Log::alert("no class");
        
$A=new $class($params);
        return 
$A->_call($method$path);
    }

    
/**
     * Get Action($path)
     * @param string $path
     * @param array $params
     * @return Action
     */
    
static function I(string $path, array $params = []): \Action {
        [
$class] = self::handler($path);
        if (!
$class) {
            
\Log::alert('no class');
        }
        return new 
$class($params);
    }

    
// execute action associated with url
    // p - params by ref
    
static public function action($path, & $p) { # template
        
static $depth=0;
        if (
$depth>3) {
            
$message "recursion is too deep: $path";
            
\Error\ErrorHandler::sentry()->captureException(
                new 
ErrorException($message)
            );
            
\Log::alert($message);
        }
        
$depth++;

        [
$class$method] = self::handler($path);
        if (! 
$method) {
            return 
$class// just a template w/o action
        
}

        
$m2=substr($method,0,2);
        
// check to see a_ or c_ or xmlhttprequest header (standard in most js frameworks)
        
$is_ajax = ($m2=='a_' || $m2=='c_' || (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower(@$_SERVER['HTTP_X_REQUESTED_WITH'])=='xmlhttprequest'));
        if (
$is_ajax) { // a_, a- is ajax
            
if (empty($_GET['profiler_key'])) #disable profiling in ajax requests always except profiler_key specified eg admin wants to debug ajax request.
                
Profiler::disable();
            
CD::set("AJAX"1);
        }

        
Profiler::in("${class}::$method"$p);
        
$A = new $class($p);

        
$r $A->_call($method$path);
        
Profiler::out();

        
//respect called action's desire to act as AJAX.
        
$is_ajax CD("AJAX");

        
// AJAX methods - default - no templates
        
if ($is_ajax) {
            if (
$r===NULL)
                die;                
// end processing
            
if (is_array($r)) {
                
//real ajax output. Put cache header only here, within the actual data output.
                //if ($m2=='a_') {
                //    headerNoCacheNoIndex();
                //}
                
if ($m2=='c_') {
                    
//Not sure that c.xxx is supported...
                    
cache_page("7d");
                }else{
                    
headerNoCacheNoIndex();
                }
                
header('Content-Type: application/json');
                echo 
json_encode($r);
                die;
            }
        }

        
// Explicit no template case
        
if ($r === 0) {
            
Profiler::disable(); // avoid breaking non standard layouts
            
die;                // end processing
        
}

        
Template::$Action=$A;   // Action Instance for use in template

        // Default Case: template based URL
        
if ($r === NULL) {
            if (
substr($path,-1)=='/'$path.="index";
            if (! 
$path) return "index";
            return 
$path;
        }

        if (
is_array($r) )      // NEW template/action, NEW params
            
return self::action($r[0], $r[1]); // Action with new params

        // Special processing
        
if (isset($r[1]) && $r[1] === ':') { // "{LETTER}:FORWARD-ACTION"
            
$a substr($r2); // action

            
if ($r[0] === 'a') { // NEW action, SAME params
                
return self::action($a$p);
            }

            try {
                if (
$r[0] === 't') { // use twig as a template engine
                    
echo self::_twig($a$p);
                    die;
                }

                
// v: - legacy implementation of Views
                
if ($r[0] === 'v') { // use h\view\View as a template engine
                    
i('view')->legacyShow($a ?: $path$p);
                    die;
                }

                
// w: - normal view implementation
                
if ($r[0] === 'w') { // use h\view\View as a template engine
                    
if (!$a) {
                        
$a $path;
                    }
                    if (
$a && $a[0] === ':') {
                        
$a substr($a1);
                        
$_p strrpos($path'/');
                        [
$path$block] = [$_p substr($path0,$_p) : 'root'$_p substr($path$_p+1) : ''];
                        
# ":block" => Directory:block          # specific block in Directory Action
                        # ":"      => Directory:CurrentMethod  # `methodName` block in Directory Action
                        
$block strtr($block, ['-' => '__''.' => '_']);
                        
$a $block "$path:$block"$path:$a";
                    }
                    
i('view')->show($a$p);
                    die;
                }
            } catch (
\hb\core\view\ViewNotFoundException $e) {
                if (
Debug::is_admin()) {
                    throw 
$e;
                }

                
do404();
            }

            if (
$r[0] === 'p') {    // NEW action, SAME params, CALL PROXY ACTION, USE PROXY TEMPLATE
                
if (CC("web-root-proxy")) { // 100% pro
                    
C_set('web-root'C("web-root-proxy"));
                    
C_set('proxy-action'0);
                    
C_set('proxy-template'0);
                } else
                    
\Log::alert("no proxy configured");
                return 
self::action($a$p);
            }

            
/*
            if ($r[0]=='r') {   // Render results on RENDER FARM
                // "r:" or "r:path"
                Profiler::disable();
                \Log::alert("TBD");
                // TODO: 1. Generate KEY
                //       2. Save Action Data to MEMCACHED(KEY)
                $KEY="xxx";
                if (! $a) $a=str_replace("__","/",$class)."/".$method;
                Profiler::in_out("Render: $a", $KEY);
                echo '<!--# include virtual="/render/?KEY=$KEY&$page=$a" -->';
                die;
            }
            */

            
\Log::alert("bad special processing directive: $r");
        }
        return 
$r;
    }

    
// @ ARE WE USING THIS PROXY
    //   IF NO - GET RID OF THIS CRAP

    // Find Action for URL
    // Include Action class
    // Return handler: [class_name, method_name]
    
static function handler($path) { # [ action_class, method ]
        
$cm=self::_class_method($path);
        
$file=str_replace( ["__""_"], ["-","/"], $cm[0]);

        
/** @todo this should be rewritten AND activated ONLY when u need this (use CD("opt"))
         *  so far ONLY 4 seeds uses this
         **/
        
foreach (self::$priorityByDirectory as $directoryData) {
            
$tmpFileName $directoryData['dir'] . $file;
            
$resourceFilePath C('web-root') . '/' $tmpFileName ".php";
            if (
file_exists($resourceFilePath)) {
                
$cm[0] = $directoryData['classPrefix'] . $cm[0];
                break;
            } else {
                
$resourceFilePath null;
            }
        }
        
$rfile$resourceFilePath ?? C('web-root').'/'.$file.".php";

        
// TODO: move all is_admin() functionality to Controller_Admin class
        
if (Debug::is_admin()) {

            if (isset(
$_GET['PHPINFO'])) {
                
phpinfo();
                exit();
            }

            if (isset(
$_GET["TPL"])) {
                
$tpath=$path;
                if (
substr($tpath,-1)=='/' || ! $tpath$tpath.="index";
                
$tfile=C('web-root')."/templates/$tpath.phtml";
                echo 
sprintf("<table cellspacing=0 cellpadding=5 border=1>
                                 <tr><td>Class<td><font color=green><b>%s</b></font>
                                 <tr><td>Method<td><b>%s</b>
                                 <tr><td>File<td><a href='edit:
$rfile'>$rfile</a>
                                 <tr><td>Template<td><a href='edit:
$tfile'>$tfile</a>
                              </table>"
,
                             
$cm[0], $cm[1]);
            }
        }

        if (! 
file_exists($rfile) ) {
            if (
CC('action-proxy') ) { /* ACTION PROXY */
                
C_set('template-proxy',1);
                
$rfile=C('web-root-proxy').'/'.$file.".php";
                if (! 
file_exists($rfile) )
                    return [
$path,""];
            } else
                return [
$path,""];
        }

        
Profiler::in($cm[0].'::'.$cm[1], $file);
        include_once(
$rfile);
        if (! 
class_exists($cm[0], false) ) { /* file exists but no class defined */
            
C_set('template-proxy',1);
            if (
class_exists($cm[0]."_"false)) {
                
$cm[0]=$cm[0]."_"/* using new class name */
                
Profiler::info("proxy class"$cm[0]);
            }
        }
        
Profiler::out();
        return 
$cm;
    }


    
// Convert URL to Action: [Class, method], include File when exists
    // URL's "-" is converted to "__" in Class Names and Methods
    // example: my/address-book/in-dex => executes Action_My_Address__book->in_dex
    
static function _class_method($path) { # [class, method]
        
if ($path && $path[0]=='/'$path=substr($path,1);

        
$dl=strrpos($path,"/");
        if (!
$dl) {             // ROOT
            
$method =str_replace(["-""."], ["__","_"], $path);
            return array(
"Action_Root"NVL($method"index"));
        }

        
$class  =substr($path0$dl);
        
$method =NVL(substr($path$dl+1), "index");
        
$method =str_replace(["-""."], ["__""_"], $method);
        
$class  =str_replace("-""__"$class);
        
#$class  =preg_replace("!/(.)!e", "'_'.ucfirst('\\1')", $class);
        
$class preg_replace_callback(
            
"!/(.)!",
            function (
$ms) { return "_".ucfirst($ms[1]); },
            
$class
            
);
        return array(
"Action_".ucfirst($class), $method);
    }

    static function 
gz_serve($data$content_type="application/x-gzip") {
        
header("Content-Type: $content_type");
        echo 
gzencode($data4); /* compression level 4 */
    
}

    
// Twig Templating - layout wrapping
    
static function twig($template, array $params) { # HTML (twig template)
        
static $loader null;
        static 
$twig null;
        if (! 
$loader) {
            
$loader = new Twig_Loader_Filesystem(C("web-root")."/twig-templates/");
            
$opts = ['auto_reload' => true'cache' => PROJECT_ROOT.'/var/tmp/twig-cache/''autoescape' => 'html''strict_variables' => 'true'];
            
$twig = new Twig_Environment($loader$opts);
        }
        return 
$twig->render($template.".html"$params);
    }

    
// Twig Templating - layout wrapping
    
static function _twig($template, array $params) { # HTML (twig template wrapped in PHTML layout)
        
if (isset($_GET['TPL'])) {
            echo 
"<div style='padding-left: 3px; background: #eee'>";
            
vvv($params); // is_admin inside
            
echo "</div>";
        }

        if (
self::$layout ) {
            
Profiler::in("Template:$template");
            
$body self::twig($template$params);
            
Profiler::out();

            
Profiler::in("Layout"self::$layout);
            
$r fetch(self::$layout, ['body' => $body]);
            
Profiler::out();

            return 
$r;
        }

        return 
self::twig($template$params);
    }
}