<?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($url, 0, $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");
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($r, 2); // 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($a, 1);
$_p = strrpos($path, '/');
[$path, $block] = [$_p ? substr($path, 0,$_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($path, 0, $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($data, 4); /* 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);
}
}