Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/recipe-fiddle.git
<?php
namespace com\whitemagicsoftware;

require "class.Cookie.php";
require "class.Browser.php";

/**
 * Represents a Client connection. This class provides functionality 
 * for cookies, authentication, and HTTP headers (including redirection).
 */
class Client extends Obj {
  private $browser;
  private $cookie;

  private $authenticationId = 0;
  private $accountId = 0;

  public function __construct() {
    $this->setBrowser( new Browser() );
    $this->setCookie( new Cookie() );

    // Persist the client's cookie.
    $this->authenticate();
  }

  /**
   * Returns the client's IP address.
   *
   * @return An IP address (IPv4 or IPv6).
   */
  private function getIp() {
    if( isset( $_SERVER[ "REMOTE_ADDR" ] ) ) {
      $ip = $_SERVER["REMOTE_ADDR"];

      if( $ip === false ) {
        if( getenv("HTTP_CLIENT_IP") ) {
          $ip = getenv("HTTP_CLIENT_IP");
        }
        else if(getenv("HTTP_X_FORWARDED_FOR")) {
          $ip = getenv("HTTP_X_FORWARDED_FOR");
        }
        else if(getenv("HTTP_X_FORWARDED")) {
          $ip = getenv("HTTP_X_FORWARDED");
        }
        else if(getenv("HTTP_FORWARDED_FOR")) {
          $ip = getenv("HTTP_FORWARDED_FOR");
        }
        else if(getenv("HTTP_FORWARDED")) {
          $ip = getenv("HTTP_FORWARDED");
        }
        else if(getenv("REMOTE_ADDR")) {
          $ip = getenv("REMOTE_ADDR");
        }
      }
    }
    else {
      $ip = "localhost";
    }

    return $ip;
  }

  /**
   * Called before any content is sent to the browser. This transmits the
   * HTTP headers that indicate the page expires immediately.
   *
   * @param $contentType The type of content being sent (typically this
   * is either HTML or XML).
   */
  public function sendHttpHeaders( $contentType = "text/html" ) {
    ini_set( "session.cache_limiter", "private" );
    session_cache_limiter( "private_no_expire" );

    // HTTP 1.1
    header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
    header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");

    // HTTP 1.0
    header('Pragma: no-cache');

    // Proxies
    header( "Expires: 0" );

    // Output type
    header( "Content-Type: $contentType; charset=UTF-8" );
  }

  /**
   * Sends the given file to the browser.
   *
   * @param $srcFilename Name of the file to send to the browser.
   * @param $dstFilename Name of the file the browser receives.
   * @param $contentType Mime type (default: application/pdf).
   *
   * @return false on an error, otherwise the number of bytes.
   */
  public function sendFile(
    $srcFilename,
    $dstFilename = "recipe-book.pdf",
    $contentType = "application/pdf" )
  {
    header( "Content-Type: $contentType" );
    header( "Content-Description: File Transfer" );
    header( "Content-Disposition: attachment; filename=\"$dstFilename\"" );
    header( "Content-Transfer-Encoding: binary" );
    header( "Cache-Control: must-revalidate" );
    header( "Content-Length: " . filesize( $srcFilename ) );
    return readfile( $srcFilename );
  }

  /**
   * Returns the cookie token for this client browser.
   *
   * @return $this->getCookie()->getCookieToken( IP Address )
   */
  public function getCookieToken() {
    return $this->getCookie()->getCookieToken( $this->getIp() );
  }

  /**
   * Sets a cookie with the given name and value.
   *
   * @param $cookieName The name of the cookie to set.
   * @param $cookieValue The value for the given cookie name.
   * @param $httpOnly false to let JavaScript read the cookie value.
   */
  public function setCookieToken(
    $cookieName, $cookieValue, $httpOnly = true ) {

    $this->getCookie()->setCookieToken(
      $cookieName, $cookieValue, $httpOnly, $this->isSecure() );
  }

  /**
   * Forces the browser to be assigned a brand new cookie.
   */
  public function generateCookieToken() {
    $this->getCookie()->generateCookieToken( $this->getIp() );
  }

  /**
   * Returns true if the client has connected through a secure connection.
   */
  private function isSecure() {
    return
      (!empty($_SERVER['HTTPS'])) &&
      $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443;
  }

  /**
   * Links the User-Agent string, cookie, and IP address to create an
   * authentication token for a recipe account.
   *
   * See also: http://cleverlogic.net/articles/kochure
   */
  private function authenticate() {
    $db = Database::get();
    $db->call( "authentication_upsert", "",
      $this->getCookieToken(),
      $this->getBrowserPlatform(),
      $this->getBrowserName(),
      $this->getBrowserVersion(),
      $this->getIp()
    );
  }

  /**
   * Returns the authentication ID for the client's cookie. This will
   * only call the database to lookup the authentication ID if the value
   * of the authentication ID is zero (indicates it hasn't been set).
   *
   * @return 0 if the authentication ID could not be found.
   */
  public function getAuthenticationId() {
    if( $this->authenticationId === 0 ) {
      $db = Database::get();
      $result = $db->call( "get_authentication_id", "id",
        $this->getCookieToken()
      );

      $this->authenticationId = isset( $result[0] ) ? $result[0]["id"] : 0;
    }

    return $this->authenticationId;
  }

  /**
   * Returns the account ID for the client's authentication ID.
   */
  public function getAccountId() {
    if( $this->accountId === 0 ) {
      $db = Database::get();
      $result = $db->call( "get_account_id", "id",
        $this->getAuthenticationId()
      );

      $this->accountId = isset( $result[0] ) ? $result[0]["id"] : 0;
    }

    return $this->accountId;
  }

  /**
   * The first numeric value in the URL represents the identifier of the
   * object to edit or view.
   *
   * @return The first number in the URL path, or 0 if the number could
   * not be found.
   */
  public function getUrlId() {
    // Parse the URI into its components (scheme, host, path, etc.).
    $url = parse_url( $_SERVER[ "REQUEST_URI" ] );

    // Return 0 if no number is found in the URL.
    $result = 0;

    // Find the first number and return that value.
    foreach( explode( "/", $url[ "path" ] ) as $id ) {
      if( is_numeric( $id ) && $id > 0 ) {
        $result = $id;
        break;
      }
    }

    return $result;
  }

  /**
   * Returns the numberic value for an identifier, given the name of a
   * POST/GET variable parameter ($name). The default return value is 0.
   * This delegates to getParameter.
   *
   * @param $name The name of the parameter to get from the client.
   * @param $v The default value to return.
   */
  function getId( $name, $v = 0 ) {
    // Remove all characters except numbers.
    return preg_replace( "/[^0-9\-]/", "",
      $this->getParameter( $name, $v ) );
  }

  /**
   * Returns the value for a parameter given by the name $name. If the
   * parameter is not set, this will return the value of $default (null by
   * default). POST parameters take precedence over GET parameters.
   * The value returned is not database-safe.
   *
   * @param $name The name of the HTTP parameter.
   * @param $v The default value to return.
   *
   * @return A version of the string value that corresponds to the
   * named POST (or GET) parameter.
   */
  public function getParameter( $name, $v = null ) {
    if( isset( $_POST[ $name ] ) && !empty( $_POST[ $name ] ) ) {
      $result = $_POST[ $name ];
    }
    else if( isset( $_GET[ $name ] ) && !empty( $_GET[ $name ] ) ) {
      $result = $_GET[ $name ];
    }

    // Normalize spaces, but not newlines.
    return empty( $result ) ?
      $v : preg_replace( "/[^\S\r\n]/", " ", $result );
  }

  /**
   * Redirects the user's browser to the correct URL for a given ID. This
   * causes the title to change. This function will exit if the URI and
   * the URL are mismatched. The title will become URL-friendly.
   *
   * @param $BASE $BASE_RECIPE, $BASE_ACCOUNT, $BASE_DIET, etc.
   * @param $id The ID being viewed or edited.
   * @param $title The title (to be URL-friendly sanitized) in the URL.
   *
   * @return false if the browser was not redirected.
   */
  public function redirect( $BASE, $id, $title ) {
    // Derive the "permanent" URL.
    $url = "$BASE$id/" . $this->sanitize( $title );

    // Get the URL from the browser.
    $uri = $_SERVER[ "REQUEST_URI" ];

    // If the browser's request URI and the URL for the given ID do not
    // match, then redirect the browser to the correct URL.
    if( $url !== $uri ) {
      // Redirect the user to the correct URL.
      header( "Location: $url", true, 302 );
      return true;
    }

    // No redirection happened.
    return false;
  }

  /**
   * Returns a sanitized URL from the user.
   *
   * @param $name Name of the parameter containing a URL to sanitize.
   */
  public function getUrl( $name ) {
    return filter_var( $this->getParameter( $name ), FILTER_SANITIZE_URL );
  }

  /**
   * Returns the given text with accented characters replaced with ASCII
   * equivalents. This won't work for Asian languages.
   */
  private function sanitize( $text ) {
    $text = $this->replaceAccents( strtolower( trim( $text ) ) );
   
    // Add spaces and union characters
    $find = array(" ", "&", "\r\n", "\n", "+",",");
    $text = str_replace( $find, "-", $text );
   
    // Delete and replace remaining special chars
    $find = array("/[^a-z0-9\-<>]/", "/[\-]+/", "/<[^>]*>/");
    $repl = array("", "-", "");
    return preg_replace( $find, $repl, $text );
  }

  /**
   * Returns true if this client is not a web browser.
   */
  public function isBrowser() {
    return !$this->getBrowser()->isRobot();
  }

  private function getBrowserPlatform() {
    return $this->getBrowser()->getPlatform();
  }

  private function getBrowserName() {
    return $this->getBrowser()->getBrowser();
  }

  private function getBrowserVersion() {
    return $this->getBrowser()->getVersion();
  }

  private function setCookie( $cookie ) {
    $this->cookie = $cookie;
  }

  private function getCookie() {
    return $this->cookie;
  }

  private function setBrowser( $browser ) {
    $this->browser = $browser;
  }

  private function getBrowser() {
    return $this->browser;
  }

  /**
   * Generates a random user agent string that can be recognized by
   * HTTP servers.
   *
   * @return A user agent string suitable for HTTP request headers.
   */
  function generateUserAgent() {
    $agents = array(
      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17",
      "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.15 (KHTML, like Gecko) Chrome/24.0.1295.0 Safari/537.15",
      "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.14 (KHTML, like Gecko) Chrome/24.0.1292.0 Safari/537.14",
      "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1284.0 Safari/537.13",
      "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
      "Mozilla/6.0 (Windows NT 6.2; WOW64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1",
      "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1",
      "Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1",
      "Mozilla/5.0 (Windows NT 6.1; rv:15.0) Gecko/20120716 Firefox/15.0a2",
      "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.16) Gecko/20120421 Gecko Firefox/11.0",
      "Mozilla/5.0 (Windows NT 6.1; U;WOW64; de;rv:11.0) Gecko Firefox/11.0",
      "Mozilla/5.0 (Windows NT 5.1; rv:15.0) Gecko/20100101 Firefox/13.0.1",
      "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0",
      "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)",
      "Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))",
      "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
      "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; GTB7.4; InfoPath.2; SV1; .NET CLR 3.3.69573; WOW64; en-US)",
      "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8)",
      "Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25",
      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10",
      "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; da-dk) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1",
      "Opera/12.80 (Windows NT 5.1; U; en) Presto/2.10.289 Version/12.02",
      "Mozilla/5.0 (Windows NT 5.1) Gecko/20100101 Firefox/14.0 Opera/12.0",
      "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; de) Presto/2.9.168 Version/11.52",
      "Opera/9.80 (X11; Linux x86_64; U; bg) Presto/2.8.131 Version/11.10"
    );

    return $agents[ array_rand( $agents ) ];
  }


  /**
   * Replaces accent characters with non-accented equivalents. This is
   * mostly used to make pretty URLs for Latin languages.
   *
   * @param $v The text to uninternationalize.
   */
  private function replaceAccents( $v ) {
    $a = array(
      'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë',
      'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', 'Ø',
      'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'ß', 'à', 'á', 'â', 'ã', 'ä', 'å',
      'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ñ', 'ò',
      'ó', 'ô', 'õ', 'ö', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'ÿ', 'Ā', 
      'ā', 'Ă', 'ă', 'Ą', 'ą', 'Ć', 'ć', 'Ĉ', 'ĉ', 'Ċ', 'ċ', 'Č', 
      'č', 'Ď', 'ď', 'Đ', 'đ', 'Ē', 'ē', 'Ĕ', 'ĕ', 'Ė', 'ė', 'Ę', 
      'ę', 'Ě', 'ě', 'Ĝ', 'ĝ', 'Ğ', 'ğ', 'Ġ', 'ġ', 'Ģ', 'ģ', 'Ĥ',
      'ĥ', 'Ħ', 'ħ', 'Ĩ', 'ĩ', 'Ī', 'ī', 'Ĭ', 'ĭ', 'Į', 'į', 'İ', 
      'ı', 'IJ', 'ij', 'Ĵ', 'ĵ', 'Ķ', 'ķ', 'Ĺ', 'ĺ', 'Ļ', 'ļ', 'Ľ',
      'ľ', 'Ŀ', 'ŀ', 'Ł', 'ł', 'Ń', 'ń', 'Ņ', 'ņ', 'Ň', 'ň', 'ʼn', 
      'Ō', 'ō', 'Ŏ', 'ŏ', 'Ő', 'ő', 'Œ', 'œ', 'Ŕ', 'ŕ', 'Ŗ', 'ŗ', 
      'Ř', 'ř', 'Ś', 'ś', 'Ŝ', 'ŝ', 'Ş', 'ş', 'Š', 'š', 'Ţ', 'ţ', 
      'Ť', 'ť', 'Ŧ', 'ŧ', 'Ũ', 'ũ', 'Ū', 'ū', 'Ŭ', 'ŭ', 'Ů', 'ů', 
      'Ű', 'ű', 'Ų', 'ų', 'Ŵ', 'ŵ', 'Ŷ', 'ŷ', 'Ÿ', 'Ź', 'ź', 'Ż', 
      'ż', 'Ž', 'ž', 'ſ', 'ƒ', 'Ơ', 'ơ', 'Ư', 'ư', 'Ǎ', 'ǎ', 'Ǐ', 
      'ǐ', 'Ǒ', 'ǒ', 'Ǔ', 'ǔ', 'Ǖ', 'ǖ', 'Ǘ', 'ǘ', 'Ǚ', 'ǚ', 'Ǜ', 
      'ǜ', 'Ǻ', 'ǻ', 'Ǽ', 'ǽ', 'Ǿ', 'ǿ'); 
    $b = array(
      'A', 'A', 'A', 'A', 'A', 'A', 'AE', 'C', 'E', 'E', 'E', 'E', 
      'I', 'I', 'I', 'I', 'D', 'N', 'O', 'O', 'O', 'O', 'O', 'O', 
      'U', 'U', 'U', 'U', 'Y', 's', 'a', 'a', 'a', 'a', 'a', 'a', 
      'ae', 'c', 'e', 'e', 'e', 'e', 'i', 'i', 'i', 'i', 'n', 'o', 
      'o', 'o', 'o', 'o', 'o', 'u', 'u', 'u', 'u', 'y', 'y', 'A', 
      'a', 'A', 'a', 'A', 'a', 'C', 'c', 'C', 'c', 'C', 'c', 'C', 
      'c', 'D', 'd', 'D', 'd', 'E', 'e', 'E', 'e', 'E', 'e', 'E', 
      'e', 'E', 'e', 'G', 'g', 'G', 'g', 'G', 'g', 'G', 'g', 'H', 
      'h', 'H', 'h', 'I', 'i', 'I', 'i', 'I', 'i', 'I', 'i', 'I', 
      'i', 'IJ', 'ij', 'J', 'j', 'K', 'k', 'L', 'l', 'L', 'l', 'L', 
      'l', 'L', 'l', 'l', 'l', 'N', 'n', 'N', 'n', 'N', 'n', 'n', 
      'O', 'o', 'O', 'o', 'O', 'o', 'OE', 'oe', 'R', 'r', 'R', 'r', 
      'R', 'r', 'S', 's', 'S', 's', 'S', 's', 'S', 's', 'T', 't', 
      'T', 't', 'T', 't', 'U', 'u', 'U', 'u', 'U', 'u', 'U', 'u', 
      'U', 'u', 'U', 'u', 'W', 'w', 'Y', 'y', 'Y', 'Z', 'z', 'Z', 
      'z', 'Z', 'z', 's', 'f', 'O', 'o', 'U', 'u', 'A', 'a', 'I', 
      'i', 'O', 'o', 'U', 'u', 'U', 'u', 'U', 'u', 'U', 'u', 'U', 
      'u', 'A', 'a', 'AE', 'ae', 'O', 'o'); 

    return str_replace( $a, $b, $v );
  }
}