<?php namespace com\whitemagicsoftware; require "constants.php"; require "class.Obj.php"; require "class.Database.php"; require "class.Client.php"; require "class.Xslt.php"; require_once "class.Normalizer.php"; /** * Used by all subclasses that interact with the end user. */ abstract class BaseController extends Obj { /** Represents the web browser client. */ private $client; /** XSLT processor that transforms results from database queries. */ private $xslt; /** Identifier for the controller object. */ private $id = 0; /** Command requested by the user (e.g., update). */ private $command = ""; /** Subcommand requested by the user (e.g., an ingredient). */ private $subcommand = ""; /** Assume the object cannot be edited until authorized. */ private $editable = false; /** Display name for the account (usually person's name). */ private $accountLabel = null; /** * Sets the connection that this class uses for database transactions. */ public function __construct() { $this->setClient( new Client() ); } /** * Only handles requests from valid browsers (i.e., not robots). * * @see setEditState * @see parseCommands * @see handleRequest */ public function run() { // Not a robot; allow the user to interact with the site. if( $this->isBrowser() ) { $this->setEditState(); $this->parseCommands(); $this->handleRequest(); } } /** * Sends HTTP headers, performs Post-Redirect-Get, and sends XHTML * if required. * * @param $prg Set to false when PRG is not required. */ protected function render( $prg = true ) { if( $_POST && $prg ) { // Redirect to the same page; overwrite existing Location headers. // (There should not be any previously sent headers...) header( "Location: " . $_SERVER["REQUEST_URI"], true, 303 ); } else { $this->sendHttpHeaders(); echo $this->getXhtml(); } } /** * Delegates calling a database function to the database singleton. * * @see Database::call */ protected function call() { $db = Database::get(); return call_user_func_array( array( $db, "call" ), func_get_args() ); } /** * Performs a json_encode on the given results (converts an array to * JSON data). This is typically used for Ajax requests. */ protected function json( $results ) { return json_encode( $results ); } /** * Delegates transforming a regular array into a string suitable for * inserting into a PostgreSQL database. */ protected function arrayToString( $array ) { $db = Database::get(); return $db->arrayToString( $array ); } /** * Delegates transforming a regular array of strings (no numbers) into * a string suitable for passing to a PostgreSQL stored procedure or * function. */ protected function toArray( $set ) { $db = Database::get(); return $db->toArray( $set ); } /** * Returns true if the client was redirected to a new URL. This * causes the URL to synchronize with a title. * * @param $BASE The base path to the application. * @param $id The numeric identifier for the account. * @param $title The title to use; if null, determined by querying $id. */ protected function redirect( $BASE, $id, $title = null ) { if( $title == null ) { $title = $this->getAccountLabel(); } return $this->getClient()->redirect( $BASE, $id, $title ); } /** * Delegates sending HTTP headers via the Client. */ protected function sendHttpHeaders( $contentType = "text/html" ) { $this->getClient()->sendHttpHeaders( $contentType ); } /** * Delegates sending a PDF via the Client. This will set all the * required HTTP headers to handle the file upload. */ protected function sendPDF( $srcFilename, $dstFilename = "recipe-book.pdf" ) { return $this->getClient()->sendFile( $srcFilename, $dstFilename, $contentType = "application/pdf" ); } /** * @see http://stackoverflow.com/a/5860054/59087 */ protected function slug( $text, $slug = '_', $extra = '.' ) { return strtolower( trim( preg_replace('~[^0-9a-z' . preg_quote($extra, '~') . ']+~i', $slug, $this->unaccent($text) ), $slug ) ); } /** * Removes accented characters from a string by performing a little * romanization. */ private function unaccent($s) { if( strpos($s = htmlentities($s, ENT_QUOTES, 'UTF-8'), '&') !== false) { $s = html_entity_decode(preg_replace('~&([a-z]{1,2})(?:acute|cedil|circ|grave|lig|orn|ring|slash|tilde|uml);~i', '$1', $s), ENT_QUOTES, 'UTF-8'); } return $s; } /** * Sets the ID for subsequent (editing) operations. * * @param $id The identifier used by user-requested commands. */ protected function setId( $id ) { if( is_numeric( $id ) && $id >= 0 ) { $this->id = $id; } } /** * Returns the ID of the object to edit. If the ID has not already * been set, this will attempt to get it from the HTTP request * parameter value corresponding to the getParameterIdName requst * variable. * * @return 0 if the ID could not be determined. */ protected function getId() { if( $this->id <= 0 ) { // HTML form ID overrides URL ID. $this->id = $this->getParameterId( $this->getParameterIdName() ); //$this->log( "parameter id = " . $this->id ); if( $this->id <= 0 ) { // URL ID overrides database ID. $this->id = $this->getUrlId(); //$this->log( "url id = " . $this->id ); // Check to see if that ID actually exists. if( $this->id <= 0 || !$this->exists( $this->id ) ) { // Neither a form ID nor URL ID were found; try to retrieve the // ID using the user's authentication ID (i.e., cookie). This // behaviour is defined in the subclass. $this->id = $this->getLastResortId(); //$this->log( "last resort id = " . $this->id ); } } } return $this->id; } /** * Determines whether or not the user can edit this item. This is * only called if the Client instance is not a robot. */ protected function setEditState() { if( $this->authorize() ) { $result = $this->call( $this->getAuthorizationFunctionName(), "auth", $this->getId(), $this->getAuthenticationId() ); $this->setEditable( isset( $result[0] ) ? $result[0]["auth"] > 0 : false ); } } /** * Returns true to indicate whether the user may edit the content. This * has a side-effect in that commands sent from the user are not parsed * if this returns false. * * \todo Split mixed functionality into isEditable and canParseCommand. * * @return false if the user cannot edit the content or commands should * not be parsed. */ protected function isEditable() { return $this->editable; } private function setEditable( $editable = false ) { if( is_bool( $editable ) ) { $this->editable = $editable; } } /** * Gets the identifier noted by the parameter name ($name). * * @param $name The name of the HTTP request parameter to retrieve. * @param $v The default value if the request parameter is not set. */ protected function getParameterId( $name, $v = 0 ) { return $this->getParameter( $name, $v ); } /** * Delegates to the client instance. */ protected function getParameter( $name, $v = null ) { return $this->getClient()->getParameter( $name, $v ); } /** * Returns a URL, normalized to be hyperlink-ready. * * @param name Retrieve the value of this parameter name. * @return The value of the parameter converted to a valid URL. */ protected function getParameterURL( $name ) { return $this->normalizeURL( $this->getParameter( $name ) ); } /** * Ensures URLs are encoded and normalized properly (according to RFC). * * @param url The URL to make conform to a hyperlink-ready URL. * @return The value of the parameter converted to a valid URL. */ private function normalizeURL( $url ) { $n = $this->getNormalizer(); $n->setUrl( $url ); return $n->normalize(); } /** * Accessor method for the normalizer. * * @return A new Normalizer instance for ensuring consistent, well-formed * URLs. */ private function getNormalizer() { return new Normalizer(); } /** * Answers whether the client connection is a browser (versus a robot). * * @return true iff the client is not a robot (e.g., Googlebot). */ public function isBrowser() { return $this->getClient()->isBrowser(); } /** * Delegates to the Client to return the authentication ID for the * client's cookie (authentication token). * * @return 0 if the account could not be authenticated. */ protected function getAuthenticationId() { return $this->getClient()->getAuthenticationId(); } /** * Delegates to the Client to return the account ID for the client's * authentication ID. * * @return 0 if the account could not be authenticated. */ protected function getAccountId() { return $this->getClient()->getAccountId(); } /** * Returns the account name. This caches the account label for this * request. * * @param $id The identifier for the account. * @return The account name, or $DEFAULT_USER_NAME if the account name * could not be found (i.e., no settings). */ protected function getAccountLabel( $id = 0 ) { if( $id == 0 ) { $id = $this->getAccountId(); } if( !isset( $this->accountLabel ) ) { global $DEFAULT_USER_NAME; $result = $this->call( "get_account_label", "label", $id ); $this->accountLabel = isset( $result[0] ) ? $result[0]["label"] : $DEFAULT_USER_NAME; } return $this->accountLabel; } /** * Changes the account name in the database. * * @param $text The new account label. * @return The new account name, or $DEFAULT_USER_NAME if not found. */ protected function setAccountLabel( $text ) { global $DEFAULT_USER_NAME; $result = $this->call( "set_account_label", "label", $this->getAccountId(), $text ); return isset( $result[0] ) ? $result[0]["label"] : $DEFAULT_USER_NAME; } /** * Returns the object identifier included as part of the URL. * * @return A number, or 0 if no identifier could be determined. */ protected function getUrlId() { return $this->getClient()->getUrlId(); } /** * Sets the object responsible for HTTP-related state control. * * @param $client The Client instance to use when discovering information * about the HTTP connection (and authentication). */ private function setClient( $client ) { $this->client = $client; } /** * Returns the object responsible for HTTP-related state control. * * @return A valid Client instance, never null. */ private function getClient() { return $this->client; } /** * Delegates setting the cookie to the client, which delegates to its * cookie class. * * @param cookieName The name of the cookie to set. * @param cookieValue The value for the given cookie name. * @param httpOnly false iff the cookie should be visible to JavaScript. */ protected function setCookieToken( $cookieName, $cookieValue, $httpOnly = true ) { $this->getClient()->setCookieToken( $cookieName, $cookieValue, $httpOnly ); } /** * Delegates retrieving the cookie for the connected browser to the * Client class. * * @see Client::getCookieToken */ protected function getCookieToken() { return $this->getClient()->getCookieToken(); } /** * Forces the client to create a new cookie token for the browser. */ protected function generateCookieToken() { $this->getClient()->generateCookieToken(); } /** * Returns a handle to the XSLT processor. */ private function setXsltEngine( $xslt ) { $this->xslt = $xslt; } /** * Returns a handle to the XSLT engine. * * @return An XSLTProcessor instance, never null. */ protected function getXsltEngine() { // Lazy init. if( !isset( $this->xslt ) ) { $this->setXsltEngine( new Xslt() ); } return $this->xslt; } /** * Returns an XML document with an empty recipe tag. * * @return A non-null string that can pass for an XML document. */ protected function getErrorXml( $element = "recipe" ) { // \todo Join these once the VIM syntax highlighting bug is fixed. return "<?xml version='1.0'?"."><$element />"; } /** * The application is developed in a command + subcommand structure. This * gets the command and subcommand parts, returning them as a list. * * If editing is authorized, determine the command. This is a quick way to * ensure that no unauthorized edits take place. * * Determine whether or not the user has edit permissions. (This checks the * cookie token against the recipe ID in the database.) If the recipe * exists then the user could be viewing: * * 1. a recipe they can edit (their own, permitted, or new); or * 2. a read-only or locked recipe. */ private function parseCommands() { $command = ($this->isEditable() ? $this->getParameter( "command" ) : "") . "-"; list( $command, $subcommand ) = explode( "-", $command ); $this->command = $command; $this->subcommand = $subcommand; } /** * Returns the main command requested by the user. This is often one * of the standard database commands (create, update, or delete). * * @return A command that indicates how the user is updating the data. */ protected function getCommand() { return $this->command; } /** * Returns the subcommand of an action requested by the user. * * @return A subcommand used to determine what database function to call. */ protected function getSubcommand() { return $this->subcommand; } /** * Determines whether or not authorization is required. */ protected function authorize() { return true; } /** * Returns true if the ID represents an existing object. This must * evaluate the identifier independently of the ID attribute from this * class. This method is used to determine whether a new object should * be created. Subclasses must implement this method. * * @param $id The ID to check. * @return true iff the given ID represents a valid database object. */ protected abstract function exists( $id ); /** * Returns the name of the parameter used to obtain the identifier for * objects to edit. For example, a POST or GET request would contain * the parameter "recipe-id" that identifies the name of the identifier * containing the recipe ID to edit. This method is used in conjunction * with getParameterValue(). * * @return String containing the name of the "identifying" parameter. */ protected abstract function getParameterIdName(); /** * Returns the most recently issued identifier for an action. * * @return 0 by default. */ protected function getLastResortId() { return 0; } /** * Called to handle a request made to the site. */ protected abstract function handleRequest(); /** * Called to generate an XHTML page. */ protected abstract function getXhtml(); /** * Returns the name of the function that can be called to determine * whether the user can perform edits on a given object. */ protected abstract function getAuthorizationFunctionName(); }