Merge branch '2.0'

This commit is contained in:
Josh Sherman 2014-09-25 21:50:26 -04:00
commit ebc5584660
7 changed files with 340 additions and 693 deletions

View file

@ -1,206 +0,0 @@
<?php
/**
* Single Entry Controller
*
* PHP version 5
*
* Licensed under The MIT License
* Redistribution of these files must retain the above copyright notice.
*
* @author Josh Sherman <josh@gravityblvd.com>
* @copyright Copyright 2007-2014, Josh Sherman
* @license http://www.opensource.org/licenses/mit-license.html
* @package PICKLES
* @link https://github.com/joshtronic/pickles
*/
/**
* Controller Class
*
* The heavy lifter of PICKLES, makes the calls to get the session and
* configuration loaded. Loads modules, serves up user authentication when the
* module asks for it, and loads the viewer that the module requested. Default
* values are present to make things easier on the user.
*
* @usage <code>new Controller();</code>
*/
class Controller extends Object
{
/**
* Constructor
*
* To save a few keystrokes, the Controller is executed as part of the
* constructor instead of via a method. You either want the Controller or
* you don't.
*/
public function __construct()
{
parent::__construct();
try
{
// Catches requests that aren't lowercase
$lowercase_request = strtolower($_REQUEST['request']);
if ($_REQUEST['request'] != $lowercase_request)
{
// @todo Rework the Browser class to handle the 301 (perhaps redirect301()) to not break other code
header('Location: ' . substr_replace($_SERVER['REQUEST_URI'], $lowercase_request, 1, strlen($lowercase_request)), true, 301);
throw new Exception();
}
// Grabs the requested page
$request = $_REQUEST['request'];
// Loads the module's information
$module_class = strtr($request, '/', '_');
$module_filename = SITE_MODULE_PATH . $request . '.php';
$module_exists = file_exists($module_filename);
// Attempts to instantiate the requested module
if ($module_exists)
{
if (class_exists($module_class))
{
$module = new $module_class;
}
}
// No module instantiated, load up a generic Module
if (!isset($module))
{
$module = new Module();
}
// Determines if we need to serve over HTTP or HTTPS
if ($module->secure == false && isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'])
{
header('Location: http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], true, 301);
throw new Exception();
}
elseif ($module->secure == true && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] == false))
{
header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], true, 301);
throw new Exception();
}
// Gets the profiler status
$profiler = $this->config->pickles['profiler'];
$profiler = $profiler === true || stripos($profiler, 'timers') !== false;
$default_method = '__default';
$role_method = null;
// Attempts to execute the default method
// @todo Seems a bit redundant, refactor
if ($default_method == $role_method || method_exists($module, $default_method))
{
// Starts a timer before the module is executed
if ($profiler)
{
Profiler::timer('module ' . $default_method);
}
$valid_request = false;
$error_message = 'An unexpected error has occurred.';
// Determines if the request method is valid for this request
if ($module->method)
{
if (!is_array($module->method))
{
$module->method = [$module->method];
}
foreach ($module->method as $method)
{
if ($_SERVER['REQUEST_METHOD'] == $method)
{
$valid_request = true;
break;
}
}
if (!$valid_request)
{
// @todo Should probably utilize that AJAX flag to determine the type of return
$error_message = 'There was a problem with your request method.';
}
}
else
{
$valid_request = true;
}
$valid_form_input = true;
if ($valid_request && $module->validate)
{
$validation_errors = $module->__validate();
if ($validation_errors)
{
$error_message = implode(' ', $validation_errors);
$valid_form_input = false;
}
}
/**
* Note to Self: When building in caching will need to let the
* module know to use the cache, either passing in a variable
* or setting it on the object
*/
if ($valid_request && $valid_form_input)
{
$module_return = $module->$default_method();
if (!is_array($module_return))
{
$module_return = $module->response;
}
else
{
$module_return = array_merge($module_return, $module->response);
}
}
// Stops the module timer
if ($profiler)
{
Profiler::timer('module ' . $default_method);
}
$display = new Display($module);
}
// Starts a timer for the display rendering
if ($profiler)
{
Profiler::timer('display render');
}
// Renders the content
$output = $display->render();
// Stops the display timer
if ($profiler)
{
Profiler::timer('display render');
}
}
catch (Exception $e)
{
$output = $e->getMessage();
}
echo $output;
// Display the Profiler's report if the stars are aligned
if ($this->config->pickles['profiler'])
{
Profiler::report();
}
}
}

View file

@ -1,96 +0,0 @@
<?php
/**
*Display Class File for PICKLES
*
* PHP version 5
*
* Licensed under The MIT License
* Redistribution of these files must retain the above copyright notice.
*
* @author Josh Sherman <josh@gravityblvd.com>
* @copyright Copyright 2007-2014, Josh Sherman
* @license http://www.opensource.org/licenses/mit-license.html
* @package PICKLES
* @link https://github.com/joshtronic/pickles
*/
/**
* Display Class
*
* If you can see it then it probably happened in here.
*/
class Display extends Object
{
/**
* Module
*
* This is the module we are attempting to display output for.
*/
public $module = null;
public function __construct($module = null)
{
if ($module && $module instanceof Module)
{
$this->module = $module;
}
}
public function render()
{
try
{
// Starts up the buffer so we can capture it
ob_start();
if (!is_array($this->module->response))
{
$this->module->response = [$this->module->response];
}
// Checks for the PHPSESSID in the query string
if (stripos($_SERVER['REQUEST_URI'], '?PHPSESSID=') === false)
{
// XHTML compliancy stuff
// @todo Wonder if this could be yanked now that we're in HTML5 land
ini_set('arg_separator.output', '&amp;');
ini_set('url_rewriter.tags', 'a=href,area=href,frame=src,input=src,fieldset=');
header('Content-type: text/html; charset=UTF-8');
}
else
{
// Redirect so Google knows to index the page without the session ID
list($request_uri, $phpsessid) = explode('?PHPSESSID=', $_SERVER['REQUEST_URI'], 2);
header('HTTP/1.1 301 Moved Permanently');
header('Location: ' . $request_uri);
throw new Exception('Requested URI contains PHPSESSID, redirecting.');
}
$response = [
'meta' => [
'status' => $this->module->status,
'message' => $this->module->message,
],
];
if ($this->module->response)
{
$response['response'] = $this->module->response;
}
header('Content-type: application/json');
$pretty = isset($_REQUEST['pretty']) ? JSON_PRETTY_PRINT : false;
echo json_encode($response, $pretty);
return ob_get_clean();
}
catch (Exception $e)
{
return $e->getMessage();
}
}
}

View file

@ -1,99 +0,0 @@
<?php
/**
* Dynamic Content Class File for PICKLES
*
* PHP version 5
*
* Licensed under The MIT License
* Redistribution of these files must retain the above copyright notice.
*
* @author Josh Sherman <josh@gravityblvd.com>
* @copyright Copyright 2007-2014, Josh Sherman
* @license http://www.opensource.org/licenses/mit-license.html
* @package PICKLES
* @link https://github.com/joshtronic/pickles
*/
/**
* Dynamic Class
*
* Handles generating links to static content that are a timestamp injected as
* to avoid hard caching. Also minifies content where applicable.
*
* Note: you will want to add a mod_rewrite line to your .htaccess to support
* the routing to the filenames with the timestamp injected:
*
* RewriteRule ^(.+)\.([\d]+)\.(css|js|gif|png|jpg|jpeg)$ /$1.$3 [NC,QSA]
*/
class Dynamic extends Object
{
/**
* Generate Reference
*
* Appends a dynamic piece of information to the passed reference in the
* form of a UNIX timestamp added to the query string.
*
* @param string $reference URI reference of the file
* @param string $failover URI reference to use if the reference can't be found
* @return string URI reference reference with dynamic content
*/
public function reference($reference, $failover = false)
{
// Checks if the URI reference is absolute, and not relative
if (substr($reference, 0, 1) == '/')
{
$query_string = '';
// Checks for ? and extracts query string
if (strstr($reference, '?'))
{
list($reference, $query_string) = explode('?', $reference);
}
// Adds the dot so the file functions can find the file
$file = '.' . $reference;
if (file_exists($file))
{
// Replaces the extension with time().extension
$parts = explode('.', $reference);
if (count($parts) == 1)
{
throw new Exception('Filename must have an extension (e.g. /path/to/file.png)');
}
else
{
end($parts);
$parts[key($parts)] = filemtime($file) . '.' . current($parts);
$reference = implode('.', $parts);
}
// Adds the query string back
if ($query_string != '')
{
$reference .= '?' . $query_string;
}
}
else
{
if ($failover != false)
{
$reference = $failover;
}
else
{
throw new Exception('Supplied reference does not exist (' . $reference . ')');
}
}
}
else
{
throw new Exception('Reference value must be absolute (e.g. /path/to/file.png)');
}
return $reference;
}
}

View file

@ -1,292 +0,0 @@
<?php
/**
* Module Class File for PICKLES
*
* PHP version 5
*
* Licensed under The MIT License
* Redistribution of these files must retain the above copyright notice.
*
* @author Josh Sherman <josh@gravityblvd.com>
* @copyright Copyright 2007-2014, Josh Sherman
* @license http://www.opensource.org/licenses/mit-license.html
* @package PICKLES
* @link https://github.com/joshtronic/pickles
*/
/**
* Module Class
*
* This is a parent class that all PICKLES modules should be extending. Each
* module can specify it's own meta data and whether or not a user must be
* properly authenticated to view the page. Currently any pages without a
* template are treated as pages being requested via AJAX and the return will
* be JSON encoded. In the future this may need to be changed out for logic
* that allows the requested module to specify what display type(s) it can use.
*/
abstract class Module extends Object
{
/**
* Page Title
*
* @var string, null by default
* @todo Abandon for $this->meta
*/
public $title = null;
/**
* Meta Description
*
* @var string, null by default
* @todo Abandon for $this->meta
*/
public $description = null;
/**
* Meta Keywords (comma separated)
*
* @var string, null by default
* @todo Abandon for $this->meta
*/
public $keywords = null;
/**
* Meta Data
*
* @var array
*/
public $meta = [
'title' => '',
'description' => '',
'keywords' => '',
];
/**
* Secure
*
* Whether or not the page should be loaded via SSL.
*
* @var boolean defaults to false
*/
public $secure = false;
/**
* Security Settings
*
* @var boolean, null by default
*/
public $security = null;
/**
* Method
*
* Request methods that are allowed to access the module.
*
* @var string or array, null by default
*/
public $method = null;
/**
* Validate
*
* Variables to validate.
*
* @var array
*/
public $validate = [];
/**
* Template
*
* This is the parent template that will be loaded if you are using the
* 'template' return type in the Display class. Parent templates are found
* in ./templates/__shared and use the phtml extension.
*
* @var string, 'index' by default
*/
public $template = 'index';
/**
* Response
*
* Array of data that will be rendered as part of the display. This is
* somewhat of a one way trip as you cannot get the variable unless you
* reference the response array explicitly, $this->response['variable']
*
* @var array
*/
public $response = [];
/**
* Output
*
* What should the class render as output? This can be a string or an array
* containing either 'json', 'rss', 'template' or 'xml'. Default is to use
* templates and if the template is not present, fall back to JSON.
*
* @var mixed string or array
*/
public $output = ['template', 'json'];
// @todo
public $status = 200;
public $message = 'OK';
public $echo = false;
public $limit = false;
public $offset = false;
public $errors = [];
// @todo if $status != 200 && $message == 'OK' ...
/**
* Constructor
*
* The constructor does nothing by default but can be passed a boolean
* variable to tell it to automatically run the __default() method. This is
* typically used when a module is called outside of the scope of the
* controller (the registration page calls the login page in this manner.
*
* @param boolean $autorun optional flag to autorun __default()
@ @param boolean $filter optional flag to disable autorun filtering
* @param boolean $valiate optional flag to disable autorun validation
*/
public function __construct($autorun = false, $filter = true, $validate = true)
{
parent::__construct(['cache', 'db']);
if ($autorun)
{
if ($filter)
{
// @todo
//$this->__filter();
}
if ($validate)
{
$errors = $this->__validate();
if (!$errors)
{
// @todo Fatal error perhaps?
exit('Errors encountered, this is a @todo for form validation when calling modules from inside of modules');
}
}
$this->__default();
}
}
/**
* Default "Magic" Method
*
* The __default() method is where you want to place any code that needs to
* be executed at runtime.
*
* @abstract
*/
abstract public function __default();
/**
* Magic Setter Method
*
* Places undefined properties into the response array as part of the
* module's payload.
*
* @param string $variable name of the variable to be set
* @param mixed $value value of the variable to be set
*/
public function __set($variable, $value)
{
$this->response[$variable] = $value;
}
/**
* Magic Getter Method
*
* Any variables not defined in this class are set in the response array
* and default to false if not defined there.
*
* @param string $name name of the variable requested
* @return mixed value of the variable or boolean false
*/
public function __get($name)
{
if (!isset($this->response[$name]))
{
return false;
}
else
{
return $this->response[$name];
}
}
/**
* Validate
*
* Internal validation for data passed to a Module. Grabs the super global
* based on the Module's request method and loops through the data using the
* Module's validation array (if present) sanity checking each variable
* against the rules.
*
* @return mixed boolean false if everything is fine or an array or errors
*/
public function __validate()
{
$errors = [];
if ($this->validate)
{
if (is_array($this->method))
{
$this->method = $this->method[0];
}
switch (strtoupper($this->method))
{
case 'GET':
$global = &$_GET;
break;
case 'POST':
$global = &$_POST;
break;
default:
$global = &$_REQUEST;
break;
}
foreach ($this->validate as $variable => $rules)
{
if (!is_array($rules) && $rules !== true)
{
$variable = $rules;
$rules = true;
}
if (isset($global[$variable]) && !String::isEmpty($global[$variable]))
{
if (is_array($rules))
{
$rule_errors = Validate::isValid($global[$variable], $rules);
if (is_array($rule_errors))
{
$errors = array_merge($errors, $rule_errors);
}
}
}
else
{
$errors[] = 'The ' . $variable . ' field is required.';
}
}
}
return $errors == [] ? false : $errors;
}
}

146
src/classes/Resource.php Normal file
View file

@ -0,0 +1,146 @@
<?php
/**
* Resource Class File for PICKLES
*
* PHP version 5
*
* Licensed under The MIT License
* Redistribution of these files must retain the above copyright notice.
*
* @author Josh Sherman <josh@gravityblvd.com>
* @copyright Copyright 2007-2014, Josh Sherman
* @license http://www.opensource.org/licenses/mit-license.html
* @package PICKLES
* @link https://github.com/joshtronic/pickles
*/
/**
* Resource Class
*
* This is a parent class that all PICKLES modules should be extending. Each
* module can specify it's own meta data and whether or not a user must be
* properly authenticated to view the page. Currently any pages without a
* template are treated as pages being requested via AJAX and the return will
* be JSON encoded. In the future this may need to be changed out for logic
* that allows the requested module to specify what display type(s) it can use.
*/
abstract class Resource extends Object
{
/**
* Secure
*
* Whether or not the page should be loaded via SSL.
*
* @var boolean defaults to false
*/
public $secure = false;
/**
* Filter
*
* Variables to filter.
*
* @var array
*/
public $filter = [];
/**
* Validate
*
* Variables to validate.
*
* @var array
*/
public $validate = [];
// @todo
public $status = 200;
public $message = 'OK';
public $echo = false;
public $limit = false;
public $offset = false;
public $errors = [];
// @todo if $status != 200 && $message == 'OK' ...
/**
* Constructor
*
* The constructor does nothing by default but can be passed a boolean
* variable to tell it to automatically run the __default() method. This is
* typically used when a module is called outside of the scope of the
* controller (the registration page calls the login page in this manner.
*/
public function __construct()
{
parent::__construct(['cache', 'db']);
}
/**
* Validate
*
* Internal validation for data passed to a Module. Grabs the super global
* based on the Module's request method and loops through the data using the
* Module's validation array (if present) sanity checking each variable
* against the rules.
*
* @return mixed boolean false if everything is fine or an array or errors
*/
public function __validate()
{
$errors = [];
if ($this->validate)
{
if (is_array($this->method))
{
$this->method = $this->method[0];
}
switch (strtoupper($this->method))
{
case 'GET':
$global = &$_GET;
break;
case 'POST':
$global = &$_POST;
break;
default:
$global = &$_REQUEST;
break;
}
foreach ($this->validate as $variable => $rules)
{
if (!is_array($rules) && $rules !== true)
{
$variable = $rules;
$rules = true;
}
if (isset($global[$variable]) && !String::isEmpty($global[$variable]))
{
if (is_array($rules))
{
$rule_errors = Validate::isValid($global[$variable], $rules);
if (is_array($rule_errors))
{
$errors = array_merge($errors, $rule_errors);
}
}
}
else
{
$errors[] = 'The ' . $variable . ' field is required.';
}
}
}
return $errors == [] ? false : $errors;
}
}

46
src/classes/Response.php Normal file
View file

@ -0,0 +1,46 @@
<?php
class Response extends Object
{
public $status = 200;
public $message = 'OK';
public $echo = false;
public $limit = false;
public $offset = false;
public $errors = false;
public $response = false;
public $profiler = false;
public function respond()
{
header('Content-type: application/json');
$meta = [
'status' => $this->status,
'message' => $this->message,
];
foreach (['echo', 'limit', 'offset', 'errors'] as $variable)
{
if ($this->$variable)
{
$meta[$variable] = $this->$variable;
}
}
$response = ['meta' => $meta];
foreach (['response', 'profiler'] as $variable)
{
if ($this->$variable)
{
$response[$variable] = $this->$variable;
}
}
$pretty = isset($_REQUEST['pretty']) ? JSON_PRETTY_PRINT : false;
exit(json_encode($response, $pretty));
}
}

148
src/classes/Router.php Normal file
View file

@ -0,0 +1,148 @@
<?php
/**
* Single Entry Router
*
* PHP version 5
*
* Licensed under The MIT License
* Redistribution of these files must retain the above copyright notice.
*
* @author Josh Sherman <josh@gravityblvd.com>
* @copyright Copyright 2007-2014, Josh Sherman
* @license http://www.opensource.org/licenses/mit-license.html
* @package PICKLES
* @link https://github.com/joshtronic/pickles
*/
/**
* Router Class
*
* The heavy lifter of PICKLES, makes the calls to get the session and
* configuration loaded. Loads modules, serves up user authentication when the
* module asks for it, and loads the viewer that the module requested. Default
* values are present to make things easier on the user.
*
* @usage <code>new Router();</code>
*/
class Router extends Object
{
/**
* Constructor
*
* To save a few keystrokes, the Controller is executed as part of the
* constructor instead of via a method. You either want the Controller or
* you don't.
*/
public function __construct()
{
parent::__construct();
$response = new Response();
try
{
// Grabs the requested page
$request = $_REQUEST['request'];
$components = explode('/', $request);
$version = array_shift($components);
$nouns = [];
$uids = [];
// Loops through the components to determine nouns and IDs
foreach ($components as $index => $component)
{
if ($index % 2)
{
$uids[end($nouns)] = $component;
}
else
{
$nouns[] = $component;
}
}
// Creates our class name
array_unshift($nouns, $version);
$class = implode('_', $nouns);
// Creates our filename
array_unshift($nouns, SITE_MODULE_PATH);
$filename = implode('/', $nouns) . '.php';
if (!file_exists($filename))
{
throw new Exception('Cannot find the file ' . $filename);
}
if (!class_exists($class))
{
throw new Exception('Cannot find the class ' . $class);
}
$resource = new $class($uids);
// Determines if we need to serve over HTTP or HTTPS
if ($resource->secure == false && isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'])
{
throw new Exception('This resource expects HTTPS communication.');
}
elseif ($resource->secure == true && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] == false))
{
throw new Exception('This resource expects HTTP communication.');
}
// Gets the profiler status
$profiler = $this->config->pickles['profiler'];
$profiler = $profiler === true || stripos($profiler, 'timers') !== false;
$method = strtolower($_SERVER['REQUEST_METHOD']);
if (!method_exists($resource, $method))
{
throw new Exception('Cannot find the method ' . $class . '::' . $method);
}
// Starts a timer before the resource is executed
if ($profiler)
{
Profiler::timer('resource ' . $method);
}
if ($resource->validate)
{
$validation_errors = $resource->__validate();
if ($validation_errors)
{
$response->status = 400;
$response->message = implode(' ', $validation_errors);
}
}
if ($response->status == 200)
{
$resource_return = $resource->$method();
if ($resource_return)
{
$response->response = $resource_return;
}
}
// Stops the resource timer
if ($profiler)
{
Profiler::timer('resource ' . $method);
}
}
catch (Exception $e)
{
$response->status = 500;
$response->message = $e->getMessage();
}
$response->respond();
}
}