Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/recipe-fiddle.git
<?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();
}