<?php
/**
 * Class implementing the Page Controller pattern for multipage forms
 *
 * PHP version 5
 *
 * LICENSE:
 *
 * Copyright (c) 2006-2010, Alexey Borzov <avb@php.net>,
 *                          Bertrand Mansion <golgote@mamasam.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *    * The names of the authors may not be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category   HTML
 * @package    HTML_QuickForm2
 * @author     Alexey Borzov <avb@php.net>
 * @author     Bertrand Mansion <golgote@mamasam.com>
 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
 * @version    SVN: $Id: Controller.php 295963 2010-03-08 14:33:43Z avb $
 * @link       http://pear.php.net/package/HTML_QuickForm2
 */

/** The class representing a page of a multipage form */
// require_once 'HTML/QuickForm2/Controller/Page.php';

/** Object wrapping around session variable used to store controller data */
// require_once 'HTML/QuickForm2/Controller/SessionContainer.php';

/** Class presenting the values stored in session by Controller as submitted ones */
// require_once 'HTML/QuickForm2/DataSource/Session.php';

/**
 * Class implementing the Page Controller pattern for multipage forms
 *
 * This class keeps track of pages and (default) action handlers for the form,
 * it manages $_SESSION container for the form values, allows setting
 * DataSources for the form as a whole and getting its value.
 *
 * @category   HTML
 * @package    HTML_QuickForm2
 * @author     Alexey Borzov <avb@php.net>
 * @author     Bertrand Mansion <golgote@mamasam.com>
 * @version    Release: @package_version@
 */
class HTML_QuickForm2_Controller implements IteratorAggregate
{
   /**
    * Key in $_REQUEST array that contains the ID of the Controller
    */
    const KEY_ID = '_qfc_id';

   /**
    * Key in $_SESSION array that contains the Controller data (needs ID substituted via sprintf())
    */
    const KEY_CONTAINER = '_%s_container';

   /**
    * Whether the form is a wizard
    * @var  boolean
    */
    protected $wizard = true;

   /**
    * Whether Controller ID should be sent in GET and POST parameters
    * @var  boolean
    */
    protected $propagate = true;

   /**
    * Controller ID
    * @var string
    */
    protected $id = null;

   /**
    * Contains the pages (instances of HTML_QuickForm2_Controller_Page) of the multipage form
    * @var array
    */
    protected $pages = array();

   /**
    * Contains the mapping of action names to handlers (objects implementing HTML_QuickForm2_Controller_Action)
    * @var array
    */
    protected $handlers = array();

   /**
    * The action extracted from HTTP request: array('page', 'action')
    * @var array
    */
    protected $actionName = null;

   /**
    * A wrapper around session variable used to store form data
    * @var HTML_QuickForm2_Controller_SessionContainer
    */
    protected $sessionContainer = null;

   /**
    * Finds a controller name in $_REQUEST
    *
    * @return string|null   Returns nulle if either a KEY_ID is not present
    *                       in $_REQUEST or KEY_CONTAINER is not present in
    *                       $_SESSION
    */
    public static function findControllerID()
    {
        if (empty($_REQUEST[self::KEY_ID])
            || empty($_SESSION[sprintf(self::KEY_CONTAINER, $_REQUEST[self::KEY_ID])])
        ) {
            return null;
        } else {
            return $_REQUEST[self::KEY_ID];
        }
    }

   /**
    * Class constructor
    *
    * Sets the form ID, whether to send this ID in POST and GET parameters,
    * wizard / non-wizard behaviour.
    *
    * Different forms should be given different IDs, as they are used to store
    * values in session. If $id is empty, the controller will try to find it
    * in $_REQUEST, throwing the exception if this fails.
    *
    * Wizard forms only allow going to the next page if all the previous ones
    * are valid.
    *
    * @param    string      Form ID
    * @param    boolean     Whether the form is a wizard
    * @param    boolean     Whether form's ID should be sent with GET and POST parameters
    * @throws   HTML_QuickForm2_NotFoundException   if ID is not given and cannot
    *               be found in $_REQUEST, or session container is empty
    */
    public function __construct($id = null, $wizard = true, $propagateId = false)
    {
        if (empty($id)) {
            $propagateId = true;
            $id          = self::findControllerID();
        }
        if (empty($id)) {
            throw new HTML_QuickForm2_NotFoundException(
                'Controller ID not available in $_REQUEST or session ' .
                'container is empty, please provide ID to constructor'
            );
        }
        $this->id        = $id;
        $this->wizard    = (bool)$wizard;
        $this->propagate = (bool)$propagateId;
    }

   /**
    * Returns whether the form is a wizard
    *
    * @return   boolean
    */
    public function isWizard()
    {
        return $this->wizard;
    }

   /**
    * Returns the form ID
    *
    * @return   string
    */
    public function getId()
    {
        return $this->id;
    }

   /**
    * Returns whether to send form id with GET and POST parameters
    *
    * @return   boolean
    */
    public function propagateId()
    {
        return $this->propagate;
    }

   /**
    * Returns the session container with the controller data
    *
    * @return   HTML_QuickForm2_Controller_SessionContainer
    */
    public function getSessionContainer()
    {
        if (empty($this->sessionContainer)) {
            $this->sessionContainer = new HTML_QuickForm2_Controller_SessionContainer($this);
        }
        return $this->sessionContainer;
    }

   /**
    * Removes the session variable containing the controller data
    */
    public function destroySessionContainer()
    {
        unset($_SESSION[sprintf(self::KEY_CONTAINER, $this->id)]);
        $this->sessionContainer = null;
    }

   /**
    * Extracts the name of the page and the action to perform with it from HTTP request data
    *
    * @return array     first element is page name, second is action name
    */
    public function getActionName()
    {
        if (is_array($this->actionName)) {
            return $this->actionName;
        }
        if (empty($this->pages)) {
            throw new HTML_QuickForm2_NotFoundException('No pages added to the form');
        }
        $names = array_map('preg_quote', array_keys($this->pages));
        $regex = '/^_qf_(' . implode('|', $names) . ')_(.+?)(_x)?$/';
        foreach (array_keys($_REQUEST) as $key) {
            if (preg_match($regex, $key, $matches)) {
                $this->actionName = array($matches[1], $matches[2]);
                break;
            }
        }
        if (!is_array($this->actionName)) {
            reset($this->pages);
            $this->actionName = array(key($this->pages), 'display');
        }
        return $this->actionName;
    }

   /**
    * Processes the request
    *
    * This finds the page, the action to perform with it and passes the action
    * to the page's handle() method.
    *
    * @throws HTML_QuickForm2_Exception
    */
    public function run()
    {
        list($page, $action) = $this->getActionName();
        return $this->pages[$page]->handle($action);
    }

   /**
    * Adds a handler for a specific action
    *
    * @param  string                            action name
    * @param  HTML_QuickForm2_Controller_Action the handler for the action
    */
    public function addHandler($actionName, HTML_QuickForm2_Controller_Action $action)
    {
        $this->handlers[$actionName] = $action;
    }

   /**
    * Handles an action
    *
    * This will be called if the page itself does not have a handler for a
    * specific action. The method also loads and uses default handlers for
    * common actions, if specific ones were not added.
    *
    * @param    HTML_QuickForm2_Controller_Page form page
    * @param    string                          action name
    * @throws   HTML_QuickForm2_NotFoundException   if handler for an action is missing
    */
    public function handle(HTML_QuickForm2_Controller_Page $page, $actionName)
    {
        if (!isset($this->handlers[$actionName])
            && in_array($actionName, array('next', 'back', 'submit', 'display', 'jump'))
        ) {
            $className = 'HTML_QuickForm2_Controller_Action_' . ucfirst($actionName);
            if (!class_exists($className)) {
                HTML_QuickForm2_Loader::loadClass($className);
            }
            $this->addHandler($actionName, new $className());
        }
        if (isset($this->handlers[$actionName])) {
            return $this->handlers[$actionName]->perform($page, $actionName);
        } else {
            throw new HTML_QuickForm2_NotFoundException(
                "Unhandled action '{$actionName}' for page '{$page->getForm()->getId()}'"
            );
        }
    }

   /**
    * Adds a new page to the form
    *
    * @param    HTML_QuickForm2_Controller_Page
    */
    public function addPage(HTML_QuickForm2_Controller_Page $page)
    {
        $pageId = $page->getForm()->getId();
        if (!empty($this->pages[$pageId])) {
            throw new HTML_QuickForm2_InvalidArgumentException(
                "Duplicate page ID '{$pageId}'"
            );
        }
        $page->setController($this);
        $this->pages[$pageId] = $page;
    }

   /**
    * Returns a page
    *
    * @param  string    Page ID
    * @return HTML_QuickForm2_Controller_Page
    * @throws HTML_QuickForm2_NotFoundException if there is no page with
    *           the given ID
    */
    public function getPage($pageId)
    {
        if (!empty($this->pages[$pageId])) {
            return $this->pages[$pageId];
        } else {
            throw new HTML_QuickForm2_NotFoundException(
                "Unknown page '{$pageId}'"
            );
        }
    }

   /**
    * Returns the page preceding the given one
    *
    * @param    HTML_QuickForm2_Controller_Page
    * @return   HTML_QuickForm2_Controller_Page|null
    */
    public function previousPage(HTML_QuickForm2_Controller_Page $reference)
    {
        $previous = null;
        foreach ($this->pages as $page) {
            if ($page === $reference) {
                return $previous;
            }
            $previous = $page;
        }
        return null;
    }

   /**
    * Returns the page following the given one
    *
    * @param    HTML_QuickForm2_Controller_Page
    * @return   HTML_QuickForm2_Controller_Page|null
    */
    public function nextPage(HTML_QuickForm2_Controller_Page $reference)
    {
        $previous = null;
        foreach ($this->pages as $page) {
            if ($previous === $reference) {
                return $page;
            }
            $previous = $page;
        }
        return null;
    }

   /**
    * Checks whether the pages of the controller are valid
    *
    * @param  HTML_QuickForm2_Controller_Page   If given, check only the pages
    *                                           before (not including) that page
    * @return bool
    */
    public function isValid(HTML_QuickForm2_Controller_Page $reference = null)
    {
        $container = $this->getSessionContainer();
        foreach ($this->pages as $id => $page) {
            if ($reference === $page) {
                return true;
            }
            if (!$container->getValidationStatus($id)) {
                // We should handle the possible situation when the user has never
                // seen a page of a non-modal multipage form
                if (!$this->isWizard()
                    && null === $container->getValidationStatus($id)
                ) {
                    // Empty Session datasource makes the form look submitted
                    $page->getForm()->setDatasources(array_merge(
                        $container->getDatasources(),
                        array(new HTML_QuickForm2_DataSource_Session(array()))
                    ));
                    // This will store the "submitted" values in session and
                    // return validation status
                    if ($page->storeValues()) {
                        continue;
                    }
                }
                return false;
            }
        }
        return true;
    }

   /**
    * Returns the first page that failed validation
    *
    * @return   HTML_QuickForm2_Controller_Page|null
    */
    public function getFirstInvalidPage()
    {
        foreach ($this->pages as $id => $page) {
            if (!$this->getSessionContainer()->getValidationStatus($id)) {
                return $page;
            }
        }
        return null;
    }

   /**
    * Adds a new data source to the Controller
    *
    * Note that Controller data sources are stored in session, so your data source
    * implementation should properly handle its (un)serialization.
    *
    * @param    HTML_QuickForm2_DataSource  Data source
    */
    public function addDataSource(HTML_QuickForm2_DataSource $datasource)
    {
        $this->getSessionContainer()->storeDatasources(
            array_merge($this->getSessionContainer()->getDatasources(),
                        array($datasource))
        );
    }

   /**
    * Returns the form's values
    *
    * @return   array
    */
    public function getValue()
    {
        $values = array();
        foreach (array_keys($this->pages) as $id) {
            $pageValues = $this->getSessionContainer()->getValues($id);
            // skip elements representing actions
            foreach ($pageValues as $key => $value) {
                if (0 !== strpos($key, '_qf')) {
                    if (isset($values[$key]) && is_array($value)) {
                        $values[$key] = self::arrayMerge($values[$key], $value);
                    } else {
                        $values[$key] = $value;
                    }
                }
            }
        }
        return $values;
    }

   /**
    * Merges two arrays
    *
    * Merges two arrays like the PHP function array_merge_recursive does,
    * the difference being that existing integer keys will not be renumbered.
    *
    * @param    array
    * @param    array
    * @return   array   resulting array
    */
    protected static function arrayMerge($a, $b)
    {
        foreach ($b as $k => $v) {
            if (!is_array($v) || isset($a[$k]) && !is_array($a[$k])) {
                $a[$k] = $v;
            } else {
                $a[$k] = self::arrayMerge(isset($a[$k])? $a[$k]: array(), $v);
            }
        }
        return $a;
    }

   /**
    * Returns an Iterator for the form's pages
    *
    * @return   ArrayIterator
    */
    public function getIterator()
    {
        return new ArrayIterator($this->pages);
    }
}
?>
