Dave Jarvis' Repositories

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

use DOMXPath;

require "constants.php";
require "class.BaseController.php";
require "class.SecureImageUpload.php";
require "class.Payment.php";

/**
 * Displays the book editing page and generates a PDF book.
 */
class Book extends BaseController {
  private $editable = false;
  private $watermark = true;

  /**
   * Returns true iff the given ID represents a valid book in the database.
   *
   * @param $id The book ID to validate.
   * @return true The ID is related to an existing book.
   */
  protected function exists( $id ) {
    return $this->isTrue( $this->call( "is_existing_book", "exists", $id ) );
  }

  /**
   * Saves changes, requests a recipe book in XML format, and
   * generates a PDF of the book using LaTeX.
   *
   * @return true if the book was created.
   */
  public function create() {
    global $EXECUTABLE_TYPESET_PDF;
    global $BOOK_DIRECTORY_ROOT;

    // Assume the book was not created.
    $result = false;

    // Get the list of recipes to include in the book.
    $recipes    = $this->arrayToString( $this->getRecipes() );
    $book_id    = $this->getParameterId( "book-id" );
    $account_id = $this->getAccountId();

    //$this->log( "recipes = $recipes" );
    //$this->log( "account_id = $account_id" );

    // Grabs the book ID and overwrites its existing recipes if needed.
    // If no recipes are provided, this will automatically compile the
    // book using all the recipes associated with the account.
    $book_id = $this->upsert( $account_id, $book_id, $recipes );

    //$this->log( "book_id = $book_id" );

    $this->setCoverImage( $book_id );

    $xslt = new Xslt();

    // Apply the book parameters.
    $this->setBookParameters( $xslt );

    $dom  = $xslt->toXmlDom( $this->getBookXml( $book_id ) );

    $xslt->setXmlDom( $dom );
    $xslt->setStylesheet( "context/xsl/context.xsl" );

    // Transform the XML version of the book into TeX.
    $tex = $xslt->transform();

    // Make a directory to put the book and ancillary files.
    $path = "$BOOK_DIRECTORY_ROOT$account_id/$book_id/";
    $this->createDirectory( $path );

    // Make each file the same name as the working directory.
    $base = "$path$book_id";

    // Save the working files for debugging purposes.
    $tex_file = "$base.tex";
    $xml_file = "$base.xml";

    // Give the ouptput file a name.
    $output_file = "$base.pdf";

    // Minor optimization (set false).
    $dom->formatOutput = false;

    $dom->save( $xml_file );
    file_put_contents( $tex_file, $tex, LOCK_EX );

    // Save the current path for later (needed for sending the PDF).
    $cwd = getcwd();

    // Change to a writable directory (location of the .tex file).
    chdir( $path );

    //$this->log( "$EXECUTABLE_TYPESET_PDF $tex_file" );

    // Convert the book to a PDF stream, which is written to the browser.
    exec( "$EXECUTABLE_TYPESET_PDF $tex_file" );

    // Return to the directory used by PHP.
    chdir( $cwd );

    // Send the file to the browser.
    if( file_exists( $output_file ) ) {
      $title = $this->getBookTitle();
      $title = $this->slug( $title ) . ".pdf";

      $TOKEN = "downloadToken";

      // Sets a cookie so that when the download begins the browser can
      // unblock the submit button (thus helping to prevent multiple clicks).
      // The false parameter allows the cookie to be exposed to JavaScript
      // (i.e., httpOnly is set to false).
      $this->setCookieToken(
        $TOKEN, $this->getParameter( $TOKEN ), false );

      $result = $this->sendPDF( $output_file, $title );
    }

    return $result;
  }

  /**
   * Sets the cover image for a book. This associates book cover filename
   * with the given book identifier in the database.
   *
   * @param book_id Reference for the book being modified.
   */
  private function setCoverImage( $book_id ) {
    $uploader = new SecureImageUpload();
    $id = $this->getAccountId();
    $appId = "book/$book_id";

    // If the photograph uploaded successfully, 
    if( $uploader->handle( "cover-image", $id, $appId ) ) {
      $filename = $uploader->getFilename();

      $this->call( "set_book_cover_image", "", $book_id, $filename );
    }
  }

  /**
   * Returns the XML that describes a book and its recipes.
   */
  private function getBookXml( $book_id ) {
    $xml = $this->call( "generate_book_xml", "x", $book_id );

    return isset( $xml ) ?
      $xml[0]["x"] : $this->getErrorXml( "recipe-book" );
  }

  /**
   * Associates a list of recipes with a book.
   *
   * @param $account_id Account used to create the book.
   * @param $book_id The book ID to update (can be empty).
   * @param $recipes The list of recipes to associate with the book.
   *
   * @return The book id associated with the given list of recipes, or 0
   * if the book could not be created.
   */
  private function upsert( $account_id, $book_id, $recipes ) {
    $title = $this->getBookTitle();

    $result = $this->call( "recipe_book_upsert", "upsert",
      $account_id,
      $book_id,
      $title,
      $recipes
    );

    return isset( $result ) ? $result[0]["upsert"] : 0;
  }

  /**
   * Returns the title given to the book.
   */
  private function getBookTitle() {
    global $DEFAULT_BOOK_TITLE;

    $title = $this->getParameter( "book-title" );

    return empty( $title ) ? $DEFAULT_BOOK_TITLE : $title;
  }

  /**
   * Returns the filename that corresponds to the given book theme ID.
   *
   * @param $bookThemeId The database ID for a book theme.
   * @return A string; "svedish" by default.
   */
  private function getBookThemeName( $bookThemeId ) {
    $result = $this->call( "get_book_theme_name", "x", $bookThemeId );

    // Return the default book if the ID was not found.
    return isset( $result ) ? $result[0]["x"] : "svedish";
  }

  /**
   * Applies various book parameters set by the user to the XSLT engine.
   */
  private function setBookParameters( $xslt ) {
    $themeId = $this->getParameterId( "book-theme" );
    $author  = $this->getParameter( "book-author" );

    // Find the filename for the book theme...
    $theme = $this->getBookThemeName( $themeId );

    $xslt->setParameter( "book-theme", $theme );
    $xslt->setParameter( "book-author", $author );
    $xslt->setParameter( "preview",
      $this->isFreeSubscription() ? false : $this->getWatermark() );
  }

  /**
   * Returns the name of the parameter used to obtain the identifier for
   * the object to edit. In this case, this method returns the identifier
   * used to get the account, which is associated with books.
   *
   * @return "account-id"
   */
  protected function getParameterIdName() {
    return "account-id";
  }

  /**
   * Returns a list of books and recipes for a given account.
   */
  private function getXml() {
    $result = $this->call( "generate_recipe_book_list_xml", "x",
      $this->getAccountId()
    );

    return isset( $result ) ? $result[0]["x"] : $this->getErrorXml();
  }

  /**
   * Returns the name of the stylesheet to use for transforming the
   * recipe from XML to XHTML.
   *
   * @return "xsl/book.xsl"
   */
  protected function getStylesheetName() {
    return "xsl/book.xsl";
  }

  /**
   * Returns book information in XHTML format.
   */
  protected function getXhtml() {
    global $DEFAULT_NAMESPACE;

    $xslt = $this->getXsltEngine();
    $xslt->setXml( $this->getXml() );
    $xslt->setStylesheet( $this->getStylesheetName() );

    $xslt->setParameter( $this->getParameterIdName(), $this->getId() );
    $xslt->setParameter( "editable", $this->isEditable() );
    $xslt->setParameter( "account-label", $this->getAccountLabel() );

    return $xslt->transform();
  }

  /**
   * Indicates whether the account has a free subscription for books.
   */
  private function isFreeSubscription() {
    $account = $this->getAccountId();
    return $this->isTrue(
      $this->call( "is_account_subscription", "exists", $account, "FREE" ) );
  }

  /**
   * Returns true if the watermark should be added to the book. If the
   * account is free, then no watermark will be added.
   */
  private function getWatermark() {
    return $this->watermark;
  }

  /**
   * Changes whether or not the watermark should be added to the book.
   */
  private function setWatermark( $watermark ) {
    $this->watermark = $watermark;
  }

  /**
   * Returns the name of the function used to determine whether or not
   * a given authentication ID can edit its associated books.
   *
   * @return true
   */
  protected function getAuthorizationFunctionName() {
    return "is_authorized_book";
  }

  /**
   * Returns the list of recipes selected by the user.
   */
  private function getRecipes() {
    return $this->getParameter( "recipe-list" );
  }

  /**
   * Returns the number of recipes selected by the user.
   */
  private function recipeTally() {
    return count( $this->getRecipes() );
  }

  /**
   * Uses the payment processor to make a purchase.
   *
   * @return true The purchase was successful. 
   */
  private function purchase() {
    // Assume payment failed.
    $result = false;

    $payment = new Payment();
    $payment->setToken( $this->getParameter( "purchaseId" ) );
    $payment->setEmail( $this->getParameter( "purchaseEmail" ) );
    $payment->setTally( $this->recipeTally() );
    $payment->setClientTotal( $this->getParameter( "purchaseAmount" ) );

    if( $payment->accept() ) {
      // Remove the book watermark.
      $this->setWatermark( false );

      // Cha-ching!
      $result = true;
    }

    return $result;
  }

  /**
   * Parses the user parameters and executes the book creation request.
   */
  protected function handleRequest() {
    $id = $this->getId();
    $command = $this->getCommand();
    $subcommand = $this->getSubCommand();

    if( $subcommand !== "label" ) {
      global $BASE_BOOK;

      // Ensure the URL is up-to-date.
      if( $this->redirect( $BASE_BOOK, $this->getAccountId() ) ) {
        return;
      }
    }

    // Purchase happens before creation because once the purchase is
    // verified, it sets the command to "create" and the watermark setting
    // to false.
    if( $command === "purchase" && $subcommand === "book" ) {
      if( $this->purchase() ) {
        $command = "create";
      }
    }

    if( $command === "create" && $subcommand === "book" ) {
      if( $this->create() !== false ) {
        // If the book was created, then don't generate a web page.
        return;
      }
    }

    if( $command !== "update" and $command !== "get" ) {
      $this->render();
    }
  }
}