<?php
|
|
namespace SparkLib\Application;
|
|
|
|
use \SparkLib\Application;
|
|
use \SparkLib\Application\Responder;
|
|
use \SparkLib\Application\Redirect;
|
|
use \SparkLib\Application\Action;
|
|
use \SparkLib\Fail;
|
|
use \SparkLib\Template;
|
|
use \SparkLib\Renderable;
|
|
use \Exception;
|
|
|
|
/**
|
|
* Child classes should provide public methods corresponding
|
|
* to actions.
|
|
*
|
|
* For now, see comments in Application.
|
|
*
|
|
* @author Brennen Bearnes <brennen@sparkfun.com>
|
|
*/
|
|
abstract class Controller {
|
|
|
|
protected $_app;
|
|
|
|
protected $_action = null;
|
|
protected $_errors = array();
|
|
protected $_layout = null;
|
|
protected $_defaultLayout = 'default.tpl.php';
|
|
protected $_template = null;
|
|
protected $_mime = 'text/html';
|
|
protected $_type = 'text/html';
|
|
protected $_headers = array(
|
|
'X-UA-Compatible: chrome=1', // adds support for Google Chrome Frame for IE - if we recommend it we need this
|
|
'X-Dinosaur-Says: RAWR',
|
|
);
|
|
|
|
protected $_defaultExtension = 'html';
|
|
|
|
protected $_dbiWrite = true;
|
|
|
|
protected $_modelInfo = array();
|
|
|
|
public static $useacl = false; // should this controller use the ACL to glean perms automatically?
|
|
|
|
/**
|
|
* @param \SparkLib\Application that's running the show.
|
|
*/
|
|
public function __construct (Application $app)
|
|
{
|
|
$this->_app = $app;
|
|
$this->postConstruct();
|
|
}
|
|
|
|
/**
|
|
* To be overloaded in child classes for doing constructor-like things;
|
|
* called in the constructor.
|
|
*
|
|
* Not called init() because I didn't want to eat any more of the available
|
|
* namespace for actions.
|
|
*
|
|
* You might, for example, set up a default responder here to be
|
|
* overridden later.
|
|
*/
|
|
protected function postConstruct () { }
|
|
|
|
/**
|
|
* Fire off an action (a public method on this controller), and do
|
|
* something appropriate with the results.
|
|
*
|
|
* $this->action() may return:
|
|
*
|
|
* A string or integer, to be printed.
|
|
* An array, to be encoded as JSON and printed.
|
|
* A Template, to be rendered and printed.
|
|
* Anything which implements SparkLib\Application\Action, which will be "fired".
|
|
* - for an example, see SparkLib\Redirect.
|
|
*
|
|
* Ideally, no output will be sent to the client outside of this method.
|
|
*
|
|
* @param $action string method to fire
|
|
* @return void
|
|
*/
|
|
public function handleResult ($result)
|
|
{
|
|
if ((! $result) && $this->hasResponse()) {
|
|
// the method didn't return anything, but it did set up a responder
|
|
// object with one or more callback functions for different content
|
|
// types
|
|
|
|
$wanted_type = $this->req()->mapType();
|
|
|
|
// fall back to defaults if we don't have a responder function for this.
|
|
// will be an extension, not a mime type.
|
|
if (! $this->_response->__isset($wanted_type))
|
|
$wanted_type = $this->_defaultExtension;
|
|
|
|
$result = $this->_response->run($wanted_type);
|
|
$this->setType(Application::$extensionToMime[$wanted_type]);
|
|
}
|
|
|
|
// Handle resources (file handles)
|
|
if ($result && is_resource($result)){
|
|
// Examine the resource to set the appropriate type.
|
|
switch (get_resource_type($result)){
|
|
case 'gd':
|
|
$this->setType('image/png');
|
|
break;
|
|
default:
|
|
$this->setType(false);
|
|
throw new Exception("unsupported resource type [" . get_resource_type($result) . "] from {$this->_action}()");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Send headers - passed off to environment for SAPI-specific
|
|
// handling
|
|
foreach ($this->_headers as $header) {
|
|
$this->_app->env()->header($header);
|
|
}
|
|
$this->_app->env()->header('Content-Type: ' . $this->getType());
|
|
|
|
// If this is a HEAD request, we don't want a response body
|
|
if ($this->req() instanceof \SparkLib\Application\Request\Head)
|
|
return;
|
|
|
|
// Render result based on type
|
|
if (is_string($result) || is_int($result)) echo $result;
|
|
elseif (is_array($result)) echo $this->jsonPrep($result);
|
|
elseif ($result instanceof Renderable) echo $result->render();
|
|
elseif ($result instanceof Action) $result->fire();
|
|
|
|
elseif (is_resource($result) && $this->getType()) {
|
|
switch ($this->getType()){
|
|
case 'image/png':
|
|
imagepng($result);
|
|
break;
|
|
default:
|
|
throw new Exception("unsupported mime type [{$this->getType()}] from {$this->_action}()");
|
|
break;
|
|
}
|
|
} elseif ($result instanceof \Generator) {
|
|
foreach ($result as $yielded) {
|
|
echo $yielded;
|
|
}
|
|
} else {
|
|
throw new Exception(
|
|
"expected string, array, resource, generator, Template, or Action from {$this->_action}(), but got a " . gettype($result)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Do json_encode() and make sure that SparkLib\Fail won't tack anything
|
|
* on to the response. This is something of a hack for the case where
|
|
* we haven't set up a respondTo()->json callback but are still returning
|
|
* an array to be turned into JSON from an action, and should only really
|
|
* matter in the development environment.
|
|
*
|
|
* TODO: Get better at setting response types based on the kind of thing
|
|
* we are actually sending back. Also, get better at using respondTo() for
|
|
* things where we need something other than text/html. - BPB
|
|
*
|
|
* @param $result array to be converted into JSON
|
|
*/
|
|
protected function jsonPrep (array &$result)
|
|
{
|
|
Fail::$errorLogAll = true;
|
|
return json_encode($result);
|
|
}
|
|
|
|
/**
|
|
* Get the controlling app for this controller
|
|
*
|
|
* @return \SparkLib\Application
|
|
*/
|
|
protected function app () { return $this->_app; }
|
|
|
|
/**
|
|
* Return the controller app's Request object, which should model the
|
|
* current GET or POST.
|
|
*
|
|
* @return SparkLib\Application\Request
|
|
*/
|
|
protected function req () { return $this->_app->req(); }
|
|
|
|
/**
|
|
* Return the controller appl's Route object, which should model the
|
|
* current route.
|
|
*/
|
|
protected function route () { return $this->_app->route(); }
|
|
|
|
/**
|
|
* Access the current template for this application/controller/action.
|
|
*
|
|
* @return \SparkLib\Template
|
|
*/
|
|
protected function template ()
|
|
{
|
|
if (! $this->_template)
|
|
$this->setTemplate();
|
|
return $this->_template;
|
|
}
|
|
|
|
/**
|
|
* Public version of template().
|
|
*
|
|
* @return \SparkLib\Template
|
|
*/
|
|
public function getTemplate ()
|
|
{
|
|
return $this->template();
|
|
}
|
|
|
|
/**
|
|
* Set the current template to either the default or a specified
|
|
* alternative.
|
|
*
|
|
* @param optional string template filename
|
|
*/
|
|
protected function setTemplate ($template = null)
|
|
{
|
|
if (null === $template) {
|
|
$template = $this->_app->action() . '.tpl.php';
|
|
} elseif (is_string($template) && (! strpos($template, '.'))) {
|
|
// bare string name, append ".tpl.php"
|
|
$template .= '.tpl.php';
|
|
}
|
|
|
|
$tpl_dir = strtolower(get_class($this->_app)) . '/' . $this->_app->controller();
|
|
|
|
if ($template instanceof Template) {
|
|
$this->_template = $template;
|
|
} else {
|
|
// new template with app & controller available:
|
|
$this->_template = new Template(
|
|
null,
|
|
array('app' => $this->_app, 'ctl' => $this)
|
|
);
|
|
$this->_template->setTemplateDirRel($tpl_dir);
|
|
$this->_template->setTemplate($template);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Access the current page layout for this application.
|
|
*
|
|
* @return Template for current layout
|
|
*/
|
|
protected function layout ()
|
|
{
|
|
if (! $this->_layout)
|
|
$this->setLayout();
|
|
return $this->_layout;
|
|
}
|
|
|
|
/**
|
|
* Public version of layout().
|
|
*
|
|
* @return \SparkLib\Template
|
|
*/
|
|
public function getLayout ()
|
|
{
|
|
return $this->layout();
|
|
}
|
|
|
|
/**
|
|
* Set the current page layout to either the default or a specified
|
|
* alternative.
|
|
*
|
|
* @param $layout_file string optional template filename
|
|
*/
|
|
protected function setLayout ($layout_file = null)
|
|
{
|
|
if (! $layout_file)
|
|
$layout_file = $this->_defaultLayout;
|
|
|
|
if (! strpos($layout_file, '.'))
|
|
$layout_file .= '.tpl.php';
|
|
|
|
$this->_layout = new Template(
|
|
null,
|
|
['app' => $this->_app,
|
|
'ctl' => $this]
|
|
);
|
|
$this->_layout->setTemplateDirRel(strtolower(get_class($this->_app)) . '/layouts');
|
|
$this->_layout->setTemplate($layout_file);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set a Content-Type
|
|
*
|
|
* @param $type string content type
|
|
* @return string content type
|
|
*/
|
|
protected function setType($type)
|
|
{
|
|
if ($type !== 'text/html')
|
|
Fail::$errorLogAll = true; // suppress some pesky HTML generation
|
|
$this->_type = $type;
|
|
return $type;
|
|
}
|
|
|
|
/**
|
|
* Reroute to a different action and return its results.
|
|
*
|
|
* As a side effect, resets the action on the current Application.
|
|
*
|
|
* TODO: Something about this is fucked. The app and the controller
|
|
* seem like they're the wrong kind of coupled now.
|
|
*
|
|
* @param string name of action to reroute to
|
|
* @return array|string|int|SparkLib\Application\Action result of re-routed action
|
|
*/
|
|
protected function reroute ($action)
|
|
{
|
|
$this->_app->setAction($action);
|
|
return $this->$action();
|
|
}
|
|
|
|
/**
|
|
* Access a Responder for this controller. Responder __set
|
|
* magic will treat properties as, more or less, filetype extensions
|
|
* corresponding to an appropriate MIME type, and expects them to be
|
|
* set to an anonymous function which returns appropriate data.
|
|
*
|
|
* There are two viable syntaxes for setting up a response:
|
|
*
|
|
* <code>
|
|
* // some action:
|
|
* public function view ()
|
|
* {
|
|
* $somedino = new FooSaurus(123);
|
|
* // interesting things expected to happen here
|
|
*
|
|
* $this->respondTo(array(
|
|
* 'xml' => function () use ($somedino) {
|
|
* return $somedino->toXml(); // yeah, this needs to be written too
|
|
* },
|
|
*
|
|
* 'jpeg' => function () use ($somedino) {
|
|
* // should headers be set here, or should that be automatic?
|
|
* return $somedino->renderJpeg(); // mostly I'm kidding
|
|
* }
|
|
* ));
|
|
*
|
|
* // notice that you don't need to return anything from the action here.
|
|
* }
|
|
* </code>
|
|
*
|
|
* Or...
|
|
*
|
|
* <code>
|
|
* public function view ()
|
|
* {
|
|
* // ...
|
|
*
|
|
* $this->respondTo()->xml = function () use ($somedino) {
|
|
* };
|
|
*
|
|
* // etc.
|
|
* }
|
|
* </code>
|
|
*/
|
|
protected function respondTo ($responses = null)
|
|
{
|
|
if (! isset($this->_response)) {
|
|
if (! isset($responses))
|
|
$responses = array();
|
|
$this->_response = new Responder($responses); // I know, I know, autovivification is scary
|
|
} elseif (is_array($responses)) {
|
|
$this->_response = new Responder($responses);
|
|
}
|
|
return $this->_response;
|
|
}
|
|
|
|
protected function hasResponse ()
|
|
{
|
|
return isset($this->_response);
|
|
}
|
|
|
|
/**
|
|
* Get a Redirect that goes back to the index with request params in p
|
|
* This is used by the login stuff that currently still lives in the main layout
|
|
* template.
|
|
*
|
|
* TODO: untangle.
|
|
*/
|
|
protected function goHome()
|
|
{
|
|
$app = $this->app();
|
|
$getstring = $app->controller() . '/' . $app->action() . $this->req()->compact();
|
|
return new \SparkLib\Application\Redirect($app->url() . '?p=' . urlencode($getstring), 401);
|
|
}
|
|
|
|
/**
|
|
* @return string current Content-Type
|
|
*/
|
|
public function getType()
|
|
{
|
|
return $this->_type;
|
|
}
|
|
|
|
/**
|
|
* Set a custom HTTP header
|
|
*
|
|
* @param string http header
|
|
* @return string http header
|
|
*/
|
|
protected function addHttpHeader ($header)
|
|
{
|
|
$this->_headers[] = $header;
|
|
return $header;
|
|
}
|
|
|
|
/**
|
|
* Attempt to create a dinosaur.
|
|
* Uses the request object as well as an optional array of presets for values
|
|
*/
|
|
protected function createModel ($presets = array())
|
|
{
|
|
if (! isset($this->_modelInfo['class'])) {
|
|
throw new Exception('Insufficient info for magick create');
|
|
}
|
|
|
|
$classname = $this->_modelInfo['class'];
|
|
|
|
if (! class_exists($classname)) {
|
|
throw new UnexpectedValueException("{$classname} is not a valid class");
|
|
}
|
|
|
|
$instance = new $classname;
|
|
|
|
$user = $_SESSION['user']->adminUser();
|
|
$instance->modificationInfo(array('user' => $user->getId()));
|
|
|
|
$pass_messages = ($this->req()->mapType() == 'html');
|
|
|
|
// Build the $source array of values for the new record by starting with the request
|
|
// object and adding key/val pairs from the presets array, if present.
|
|
// Do not allow a value $presets to overwrite a value in the request object.
|
|
$source = $this->req()->getArray();
|
|
foreach ($presets as $field => $value) {
|
|
if (! isset($source[$field]))
|
|
$source[$field] = $value;
|
|
}
|
|
|
|
try {
|
|
$instance->setFrom($source);
|
|
$instance->insert();
|
|
if ($pass_messages)
|
|
$this->_app->passMessage('Created', 'success');
|
|
|
|
return $instance;
|
|
}
|
|
catch (SparkRecordException $e) {
|
|
if ($pass_messages)
|
|
$this->_app->passMessage('Create failed', 'error');
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempt to update an associated dinosaur.
|
|
*/
|
|
protected function updateModel ()
|
|
{
|
|
if (! isset($this->_modelInfo['class'])) {
|
|
throw new Exception('Insufficient info for magick update');
|
|
}
|
|
|
|
$classname = $this->_modelInfo['class'];
|
|
|
|
if (! class_exists($classname)) {
|
|
throw new UnexpectedValueException("{$classname} is not a valid class");
|
|
}
|
|
|
|
$req = $this->req()->getArray();
|
|
$user = $_SESSION['user']->adminUser();
|
|
$instance = new $classname($this->req()->id);
|
|
|
|
if (is_array($this->_modelInfo['perms'])) {
|
|
foreach ($this->_modelInfo['perms'] as $field => $perm) {
|
|
|
|
if (isset($req[$field]) && ! $user->hasPermission(key($perm), current($perm)) &&
|
|
|
|
// xxx this is duplicating some logic in dino&update, asking if the
|
|
// field will change. i don't like it, but i don't know how
|
|
// else to stop this from throwing false warnings.
|
|
$req[$field] != $instance->$field
|
|
|
|
) {
|
|
$this->_app->passMessage("You are not allowed to update {$field}", 'error');
|
|
unset($req[$field]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// use loose comparison here to deal with the fact that we're getting
|
|
// strings but the models have actual typed values (we don't want to set
|
|
// identical values to the ones that're already set):
|
|
$fields = array_keys($req);
|
|
foreach ($fields as $field) {
|
|
if (! $instance->isValidField($field))
|
|
continue;
|
|
|
|
if ($req[$field] == $instance->$field) {
|
|
unset($req[$field]);
|
|
}
|
|
}
|
|
|
|
$instance->setFrom($req);
|
|
$instance->modificationInfo(array('user' => $user->getId()));
|
|
|
|
$pass_messages = ($this->req()->mapType() == 'html');
|
|
|
|
try {
|
|
if ($instance->update()) {
|
|
if ($pass_messages)
|
|
$this->_app->passMessage('Updated', 'success');
|
|
}
|
|
else {
|
|
if ($pass_messages)
|
|
$this->_app->passMessage('Nothing to update', 'message');
|
|
}
|
|
|
|
return $instance;
|
|
}
|
|
catch (SparkRecordException $e) {
|
|
if ($pass_messages)
|
|
$this->_app->passMessage('Update failed', 'error');
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a specific module/action
|
|
*/
|
|
public function getModule ($module)
|
|
{
|
|
if(! class_exists('\Spark\Module\\' . $module))
|
|
throw new Exception('Invalid Module: ' . $module);
|
|
|
|
$class = '\Spark\Module\\' . $module;
|
|
|
|
$module = new $class(array(
|
|
'ctl' => $this,
|
|
'app' => $this->_app
|
|
));
|
|
|
|
return $module;
|
|
}
|
|
|
|
/**
|
|
* Get the current HTTP headers
|
|
*
|
|
* @return array http headers
|
|
*/
|
|
public function getHttpHeaders ()
|
|
{
|
|
return $this->_headers;
|
|
}
|
|
|
|
/*
|
|
* Return a Link for a given action on the current controller.
|
|
* Defaults to the bare controller, which means action will be routed to index.
|
|
*
|
|
* TODO: Make this actually respect the current controller (this class) instead
|
|
* of what Application thinks is the current controller... This is too
|
|
* tightly coupled.
|
|
*
|
|
* @param $action string optional action name (uses default otherwise)
|
|
* @return SparkLib\Application\Link
|
|
*/
|
|
public function linkAction ($action = null)
|
|
{
|
|
$link = $this->_app->link($this->_app->controller());
|
|
if ($action)
|
|
$link->action($action);
|
|
return $link;
|
|
}
|
|
|
|
}
|