<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * Contains the Pager_Common class
 *
 * PHP versions 4 and 5
 *
 * LICENSE: Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. 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.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "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 FREEBSD PROJECT 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   Pager
 * @author    Lorenzo Alberton <l.alberton@quipo.it>
 * @author    Richard Heyes <richard@phpguru.org>
 * @copyright 2003-2007 Lorenzo Alberton, Richard Heyes
 * @license   http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
 * @version   CVS: $Id$
 * @link      http://pear.php.net/package/Pager
 */

/**
 * Two constants used to guess the path- and file-name of the page
 * when the user doesn't set any other value
 */
if (substr($_SERVER['PHP_SELF'], -1) == '/') {
    $http = (isset($_SERVER['HTTPS']) && ('on' == strtolower($_SERVER['HTTPS']))) ? 'https://' : 'http://';
    define('PAGER_CURRENT_FILENAME', '');
    define('PAGER_CURRENT_PATHNAME', $http.$_SERVER['HTTP_HOST'].str_replace('\\', '/', $_SERVER['PHP_SELF']));
} else {
    define('PAGER_CURRENT_FILENAME', preg_replace('/(.*)\?.*/', '\\1', basename($_SERVER['PHP_SELF'])));
    define('PAGER_CURRENT_PATHNAME', str_replace('\\', '/', dirname($_SERVER['PHP_SELF'])));
}
/**
 * Error codes
 */
define('PAGER_OK',                         0);
define('ERROR_PAGER',                     -1);
define('ERROR_PAGER_INVALID',             -2);
define('ERROR_PAGER_INVALID_PLACEHOLDER', -3);
define('ERROR_PAGER_INVALID_USAGE',       -4);
define('ERROR_PAGER_NOT_IMPLEMENTED',     -5);

/**
 * Pager_Common - Common base class for [Sliding|Jumping] Window Pager
 * Extend this class to write a custom paging class
 *
 * @category  HTML
 * @package   Pager
 * @author    Lorenzo Alberton <l.alberton@quipo.it>
 * @author    Richard Heyes <richard@phpguru.org>
 * @copyright 2003-2007 Lorenzo Alberton, Richard Heyes
 * @license   http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
 * @link      http://pear.php.net/package/Pager
 */
class Pager_Common
{
    // {{{ class vars

    /**
     * @var integer number of items
     * @access private
     */
    var $_totalItems;

    /**
     * @var integer number of items per page
     * @access private
     */
    var $_perPage     = 10;

    /**
     * @var integer number of page links for each window
     * @access private
     */
    var $_delta       = 10;

    /**
     * @var integer current page number
     * @access private
     */
    var $_currentPage = 1;

    /**
     * @var integer total pages number
     * @access private
     */
    var $_totalPages  = 1;

    /**
     * @var string CSS class for links
     * @access private
     */
    var $_linkClass   = '';

    /**
     * @var string wrapper for CSS class name
     * @access private
     */
    var $_classString = '';

    /**
     * @var string path name
     * @access private
     */
    var $_path        = PAGER_CURRENT_PATHNAME;

    /**
     * @var string file name
     * @access private
     */
    var $_fileName    = PAGER_CURRENT_FILENAME;

    /**
     * @var boolean If false, don't override the fileName option. Use at your own risk.
     * @access private
     */
    var $_fixFileName = true;

    /**
     * @var boolean you have to use FALSE with mod_rewrite
     * @access private
     */
    var $_append      = true;

    /**
     * @var string specifies which HTTP method to use
     * @access private
     */
    var $_httpMethod  = 'GET';

    /**
     * @var string specifies which HTML form to use
     * @access private
     */
    var $_formID      = '';

    /**
     * @var boolean whether or not to import submitted data
     * @access private
     */
    var $_importQuery = true;

    /**
     * @var string name of the querystring var for pageID
     * @access private
     */
    var $_urlVar      = 'pageID';

    /**
     * @var array data to pass through the link
     * @access private
     */
    var $_linkData    = array();

    /**
     * @var array additional URL vars
     * @access private
     */
    var $_extraVars   = array();

    /**
     * @var array URL vars to ignore
     * @access private
     */
    var $_excludeVars = array();

    /**
     * @var boolean TRUE => expanded mode (for Pager_Sliding)
     * @access private
     */
    var $_expanded    = true;

    /**
     * @var boolean TRUE => show accesskey attribute on <a> tags
     * @access private
     */
    var $_accesskey   = false;

    /**
     * @var string extra attributes for the <a> tag
     * @access private
     */
    var $_attributes  = '';

    /**
     * @var string onclick
     * @access private
     */
    var $_onclick = '';

    /**
     * @var string alt text for "first page" (use "%d" placeholder for page number)
     * @access private
     */
    var $_altFirst     = 'first page';

    /**
     * @var string alt text for "previous page"
     * @access private
     */
    var $_altPrev     = 'previous page';

    /**
     * @var string alt text for "next page"
     * @access private
     */
    var $_altNext     = 'next page';

    /**
     * @var string alt text for "last page" (use "%d" placeholder for page number)
     * @access private
     */
    var $_altLast     = 'last page';

    /**
     * @var string alt text for "page" (use optional "%d" placeholder for page number)
     * @access private
     */
    var $_altPage     = 'page';

    /**
     * @var string image/text to use as "prev" link
     * @access private
     */
    var $_prevImg     = '&lt;&lt; Back';

    /**
     * image/text to use as "prev" link when no prev link is needed  (e.g. on the first page)
     * NULL deactivates it
     *
     * @var string
     * @access private
     */
    var $_prevImgEmpty = null;

    /**
     * @var string image/text to use as "next" link
     * @access private
     */
    var $_nextImg     = 'Next &gt;&gt;';

    /**
     * image/text to use as "next" link when
     * no next link is needed (e.g. on the last page)
     * NULL deactivates it
     *
     * @var string
     * @access private
     */
    var $_nextImgEmpty = null;

    /**
     * @var string link separator
     * @access private
     */
    var $_separator   = '';

    /**
     * @var integer number of spaces before separator
     * @access private
     */
    var $_spacesBeforeSeparator = 0;

    /**
     * @var integer number of spaces after separator
     * @access private
     */
    var $_spacesAfterSeparator  = 1;

    /**
     * @var string HTML tag to wrap each page link
     * (use 'li' for pagination in a list)
     * @access private
     */
    var $_linkContainer  = '';

    /**
     * @var string HTML tag used to combine _linkContainer and _curLinkContainerClassName 
     * @access private
     */
    var $_linkContainerPre  = '';

    /**
     * @var string CSS class name for linkContainer
     * @access private
     */
    var $_curLinkContainerClassName  = '';

    /**
     * @var string HTML tag to wrap current page link
     * (use 'a' to keep it like all other links and set a class with $_curPageLinkClassName)
     * defaults to 'span' for backwards compatibility
     * @access private
     */
    var $_curTag  = 'span';

    /**
     * @var string CSS class name for current page link
     * @access private
     */
    var $_curPageLinkClassName  = '';

    /**
     * @var string Text before current page link
     * @access private
     */
    var $_curPageSpanPre        = '';

    /**
     * @var string Text after current page link
     * @access private
     */
    var $_curPageSpanPost       = '';

    /**
     * @var string Text before first page link
     * @access private
     */
    var $_firstPagePre  = '[';

    /**
     * @var string Text to be used for first page link
     * @access private
     */
    var $_firstPageText = '';

    /**
     * @var string Text after first page link
     * @access private
     */
    var $_firstPagePost = ']';

    /**
     * @var string Text before last page link
     * @access private
     */
    var $_lastPagePre   = '[';

    /**
     * @var string Text to be used for last page link
     * @access private
     */
    var $_lastPageText  = '';

    /**
     * @var string Text after last page link
     * @access private
     */
    var $_lastPagePost  = ']';

    /**
     * @var string Will contain the HTML code for the spaces
     * @access private
     */
    var $_spacesBefore  = '';

    /**
     * @var string Will contain the HTML code for the spaces
     * @access private
     */
    var $_spacesAfter   = '';

    /**
     * @var string String used as title in <link rel="first"> tag
     * @access private
     */
    var $_firstLinkTitle = 'first page';

    /**
     * @var string String used as title in <link rel="next"> tag
     * @access private
     */
    var $_nextLinkTitle = 'next page';

    /**
     * @var string String used as title in <link rel="previous"> tag
     * @access private
     */
    var $_prevLinkTitle = 'previous page';

    /**
     * @var string String used as title in <link rel="last"> tag
     * @access private
     */
    var $_lastLinkTitle = 'last page';

    /**
     * @var string Text to be used for the 'show all' option in the select box
     * @access private
     */
    var $_showAllText   = '';

    /**
     * @var array data to be paged
     * @access private
     */
    var $_itemData      = null;

    /**
     * @var boolean If TRUE and there's only one page, links aren't shown
     * @access private
     */
    var $_clearIfVoid   = true;

    /**
     * @var boolean Use session for storing the number of items per page
     * @access private
     */
    var $_useSessions   = false;

    /**
     * @var boolean Close the session when finished reading/writing data
     * @access private
     */
    var $_closeSession  = false;

    /**
     * @var string name of the session var for number of items per page
     * @access private
     */
    var $_sessionVar    = 'setPerPage';

    /**
     * Pear error mode (when raiseError is called)
     * (see PEAR doc)
     *
     * @var integer $_pearErrorMode
     * @access private
     */
    var $_pearErrorMode = null;

    // }}}
    // {{{ public vars

    /**
     * @var string Complete set of links
     * @access public
     */
    var $links = '';

    /**
     * @var string Complete set of link tags
     * @access public
     */
    var $linkTags = '';

    /**
     * @var array Complete set of raw link tags
     * @access public
     */
    var $linkTagsRaw = array();

    /**
     * @var array Array with a key => value pair representing
     *            page# => bool value (true if key==currentPageNumber).
     *            can be used for extreme customization.
     * @access public
     */
    var $range = array();

    /**
     * @var array list of available options (safety check)
     * @access private
     */
    var $_allowed_options = array(
        'totalItems',
        'perPage',
        'delta',
        'linkClass',
        'path',
        'fileName',
        'fixFileName',
        'append',
        'httpMethod',
        'formID',
        'importQuery',
        'urlVar',
        'altFirst',
        'altPrev',
        'altNext',
        'altLast',
        'altPage',
        'prevImg',
        'prevImgEmpty',
        'nextImg',
        'nextImgEmpty',
        'expanded',
        'accesskey',
        'attributes',
        'onclick',
        'separator',
        'spacesBeforeSeparator',
        'spacesAfterSeparator',
        'linkContainer',
        'curLinkContainerClassName',
        'curTag',
        'curPageLinkClassName',
        'curPageSpanPre',
        'curPageSpanPost',
        'firstPagePre',
        'firstPageText',
        'firstPagePost',
        'lastPagePre',
        'lastPageText',
        'lastPagePost',
        'firstLinkTitle',
        'nextLinkTitle',
        'prevLinkTitle',
        'lastLinkTitle',
        'showAllText',
        'itemData',
        'clearIfVoid',
        'useSessions',
        'closeSession',
        'sessionVar',
        'pearErrorMode',
        'extraVars',
        'excludeVars',
        'currentPage',
    );

    // }}}
    // {{{ build()

    /**
     * Generate or refresh the links and paged data after a call to setOptions()
     *
     * @return void
     * @access public
     */
    function build()
    {
        //reset
        $this->_pageData   = array();
        $this->links       = '';
        $this->linkTags    = '';
        $this->linkTagsRaw = array();

        $this->_generatePageData();
        $this->_setFirstLastText();

        if ($this->_totalPages > (2 * $this->_delta + 1)) {
            $this->links .= $this->_printFirstPage();
        }

        $this->links .= $this->_getBackLink();
        $this->links .= $this->_getPageLinks();
        $this->links .= $this->_getNextLink();

        $this->linkTags .= $this->_getFirstLinkTag();
        $this->linkTags .= $this->_getPrevLinkTag();
        $this->linkTags .= $this->_getNextLinkTag();
        $this->linkTags .= $this->_getLastLinkTag();
        
        $this->linkTagsRaw['first'] = $this->_getFirstLinkTag(true);
        $this->linkTagsRaw['prev']  = $this->_getPrevLinkTag(true);
        $this->linkTagsRaw['next']  = $this->_getNextLinkTag(true);
        $this->linkTagsRaw['last']  = $this->_getLastLinkTag(true);

        if ($this->_totalPages > (2 * $this->_delta + 1)) {
            $this->links .= $this->_printLastPage();
        }
    }

    // }}}
    // {{{ getPageData()

    /**
     * Returns an array of current pages data
     *
     * @param integer $pageID Desired page ID (optional)
     *
     * @return array Page data
     * @access public
     */
    function getPageData($pageID = null)
    {
        $pageID = empty($pageID) ? $this->_currentPage : $pageID;

        if (!isset($this->_pageData)) {
            $this->_generatePageData();
        }
        if (!empty($this->_pageData[$pageID])) {
            return $this->_pageData[$pageID];
        }
        return array();
    }

    // }}}
    // {{{ getPageIdByOffset()

    /**
     * Returns pageID for given offset
     *
     * @param integer $index Offset to get pageID for
     *
     * @return integer PageID for given offset
     * @access public
     */
    function getPageIdByOffset($index)
    {
        $msg = 'function "getPageIdByOffset()" not implemented.';
        return $this->raiseError($msg, ERROR_PAGER_NOT_IMPLEMENTED);
    }

    // }}}
    // {{{ getOffsetByPageId()

    /**
     * Returns offsets for given pageID. Eg, if you
     * pass it pageID one and your perPage limit is 10
     * it will return (1, 10). PageID of 2 would
     * give you (11, 20).
     *
     * @param integer $pageID PageID to get offsets for
     *
     * @return array  First and last offsets
     * @access public
     */
    function getOffsetByPageId($pageID = null)
    {
        $pageID = isset($pageID) ? $pageID : $this->_currentPage;
        if (!isset($this->_pageData)) {
            $this->_generatePageData();
        }

        if (isset($this->_pageData[$pageID]) || is_null($this->_itemData)) {
            return array(
                        max(($this->_perPage * ($pageID - 1)) + 1, 1),
                        min($this->_totalItems, $this->_perPage * $pageID)
                   );
        }
        return array(0, 0);
    }

    // }}}
    // {{{ getPageRangeByPageId()

    /**
     * Given a PageId, it returns the limits of the range of pages displayed.
     *
     * @param integer $pageID PageID to get offsets for
     *
     * @return array First and last offsets
     * @access public
     */
    function getPageRangeByPageId($pageID = null)
    {
        $msg = 'function "getPageRangeByPageId()" not implemented.';
        return $this->raiseError($msg, ERROR_PAGER_NOT_IMPLEMENTED);
    }

    // }}}
    // {{{ getLinks()

    /**
     * Returns back/next/first/last and page links,
     * both as ordered and associative array.
     *
     * NB: in original PEAR::Pager this method accepted two parameters,
     * $back_html and $next_html. Now the only parameter accepted is
     * an integer ($pageID), since the html text for prev/next links can
     * be set in the factory. If a second parameter is provided, then
     * the method act as it previously did. This hack was done to mantain
     * backward compatibility only.
     *
     * @param integer $pageID    Optional pageID. If specified, links for that
     *                           page are provided instead of current one.
     *                           [ADDED IN NEW PAGER VERSION]
     * @param string  $next_html HTML to put inside the next link
     *                           [deprecated: use the factory instead]
     *
     * @return array back/next/first/last and page links
     * @access public
     */
    function getLinks($pageID=null, $next_html='')
    {
        $msg = 'function "getLinks()" not implemented.';
        return $this->raiseError($msg, ERROR_PAGER_NOT_IMPLEMENTED);
    }

    // }}}
    // {{{ getCurrentPageID()

    /**
     * Returns ID of current page
     *
     * @return integer ID of current page
     * @access public
     */
    function getCurrentPageID()
    {
        return $this->_currentPage;
    }

    // }}}
    // {{{ getNextPageID()

    /**
     * Returns next page ID. If current page is last page
     * this function returns FALSE
     *
     * @return mixed Next page ID or false
     * @access public
     */
    function getNextPageID()
    {
        return ($this->getCurrentPageID() == $this->numPages() ? false : $this->getCurrentPageID() + 1);
    }

    // }}}
    // {{{ getPreviousPageID()

    /**
     * Returns previous page ID. If current page is first page
     * this function returns FALSE
     *
     * @return mixed Previous page ID or false
     * @access public
     */
    function getPreviousPageID()
    {
        return $this->isFirstPage() ? false : $this->getCurrentPageID() - 1;
    }

    // }}}
    // {{{ numItems()

    /**
     * Returns number of items
     *
     * @return integer Number of items
     * @access public
     */
    function numItems()
    {
        return $this->_totalItems;
    }

    // }}}
    // {{{ numPages()

    /**
     * Returns number of pages
     *
     * @return integer Number of pages
     * @access public
     */
    function numPages()
    {
        return (int)$this->_totalPages;
    }

    // }}}
    // {{{ isFirstPage()

    /**
     * Returns whether current page is first page
     *
     * @return bool First page or not
     * @access public
     */
    function isFirstPage()
    {
        return ($this->_currentPage < 2);
    }

    // }}}
    // {{{ isLastPage()

    /**
     * Returns whether current page is last page
     *
     * @return bool Last page or not
     * @access public
     */
    function isLastPage()
    {
        return ($this->_currentPage == $this->_totalPages);
    }

    // }}}
    // {{{ isLastPageComplete()

    /**
     * Returns whether last page is complete
     *
     * @return bool Last age complete or not
     * @access public
     */
    function isLastPageComplete()
    {
        return !($this->_totalItems % $this->_perPage);
    }

    // }}}
    // {{{ _generatePageData()

    /**
     * Calculates all page data
     *
     * @return void
     * @access private
     */
    function _generatePageData()
    {
        // Been supplied an array of data?
        if (!is_null($this->_itemData)) {
            $this->_totalItems = count($this->_itemData);
        }
        $this->_totalPages = ceil((float)$this->_totalItems / (float)$this->_perPage);
        $i = 1;
        if (!empty($this->_itemData)) {
            foreach ($this->_itemData as $key => $value) {
                $this->_pageData[$i][$key] = $value;
                if (count($this->_pageData[$i]) >= $this->_perPage) {
                    $i++;
                }
            }
        } else {
            $this->_pageData = array();
        }

        //prevent URL modification
        $this->_currentPage = min($this->_currentPage, $this->_totalPages);
    }

    // }}}
    // {{{ _renderLink()

    /**
     * Renders a link using the appropriate method
     *
     * @param string $altText  Alternative text for this link (title property)
     * @param string $linkText Text contained by this link
     *
     * @return string The link in string form
     * @access private
     */
    function _renderLink($altText, $linkText)
    {
        if ($this->_httpMethod == 'GET') {
            if ($this->_append) {
                $href = '?' . $this->_http_build_query_wrapper($this->_linkData);
            } else {
                $href = str_replace('%d', $this->_linkData[$this->_urlVar], $this->_fileName);
            }
            $onclick = '';
            if (array_key_exists($this->_urlVar, $this->_linkData)) {
                $onclick = str_replace('%d', $this->_linkData[$this->_urlVar], $this->_onclick);
            }
            return sprintf('%s<a href="%s"%s%s%s%s title="%s">%s</a>%s',
                           empty($this->_linkContainer) ? '' : '<'.$this->_linkContainer.'>',
                           htmlentities($this->_url . $href, ENT_COMPAT, 'UTF-8'),
                           empty($this->_classString) ? '' : ' '.$this->_classString,
                           empty($this->_attributes)  ? '' : ' '.$this->_attributes,
                           empty($this->_accesskey)   ? '' : ' accesskey="'.$this->_linkData[$this->_urlVar].'"',
                           empty($onclick)            ? '' : ' onclick="'.$onclick.'"',
                           $altText,
                           $linkText,
                           empty($this->_linkContainer) ? '' : '</'.$this->_linkContainer.'>'
            );
        } elseif ($this->_httpMethod == 'POST') {
            $href = $this->_url;
            if (!empty($_GET)) {
                $href .= '?' . $this->_http_build_query_wrapper($_GET);
            }
            return sprintf("%s<a href='javascript:void(0)' onclick='%s'%s%s%s title='%s'>%s</a>%s",
                           empty($this->_linkContainer) ? '' : '<'.$this->_linkContainer.'>',
                           $this->_generateFormOnClick($href, $this->_linkData),
                           empty($this->_classString) ? '' : ' '.$this->_classString,
                           empty($this->_attributes)  ? '' : ' '.$this->_attributes,
                           empty($this->_accesskey)   ? '' : ' accesskey=\''.$this->_linkData[$this->_urlVar].'\'',
                           $altText,
                           $linkText,
                           empty($this->_linkContainer) ? '' : '<'.$this->_linkContainer.'>'
            );
        }
        return '';
    }

    // }}}
    // {{{ _generateFormOnClick()

    /**
     * Mimics http_build_query() behavior in the way the data
     * in $data will appear when it makes it back to the server.
     *  For example:
     * $arr =  array('array' => array(array('hello', 'world'),
     *                                'things' => array('stuff', 'junk'));
     * http_build_query($arr)
     * and _generateFormOnClick('foo.php', $arr)
     * will yield
     * $_REQUEST['array'][0][0] === 'hello'
     * $_REQUEST['array'][0][1] === 'world'
     * $_REQUEST['array']['things'][0] === 'stuff'
     * $_REQUEST['array']['things'][1] === 'junk'
     *
     * However, instead of  generating a query string, it generates
     * Javascript to create and submit a form.
     *
     * @param string $formAction where the form should be submitted
     * @param array  $data       the associative array of names and values
     *
     * @return string A string of javascript that generates a form and submits it
     * @access private
     */
    function _generateFormOnClick($formAction, $data)
    {
        // Check we have an array to work with
        if (!is_array($data)) {
            trigger_error(
                '_generateForm() Parameter 1 expected to be Array or Object. Incorrect value given.',
                E_USER_WARNING
            );
            return false;
        }

        if (!empty($this->_formID)) {
            $str = 'var form = document.getElementById("'.$this->_formID.'"); var input = ""; ';
        } else {
            $str = 'var form = document.createElement("form"); var input = ""; ';
        }

        // We /shouldn't/ need to escape the URL ...
        $str .= sprintf('form.action = "%s"; ', htmlentities($formAction, ENT_COMPAT, 'UTF-8'));
        $str .= sprintf('form.method = "%s"; ', $this->_httpMethod);
        foreach ($data as $key => $val) {
            $str .= $this->_generateFormOnClickHelper($val, $key);
        }

        if (empty($this->_formID)) {
            $str .= 'document.getElementsByTagName("body")[0].appendChild(form);';
        }

        $str .= 'form.submit(); return false;';
        return $str;
    }

    // }}}
    // {{{ _generateFormOnClickHelper

    /**
     * This is used by _generateFormOnClick().
     * Recursively processes the arrays, objects, and literal values.
     *
     * @param mixed  $data Data that should be rendered
     * @param string $prev The name so far
     *
     * @return string A string of Javascript that creates form inputs
     *                representing the data
     * @access private
     */
    function _generateFormOnClickHelper($data, $prev = '')
    {
        $str = '';
        if (is_array($data) || is_object($data)) {
            // foreach key/visible member
            foreach ((array)$data as $key => $val) {
                // append [$key] to prev
                $tempKey = sprintf('%s[%s]', $prev, $key);
                $str .= $this->_generateFormOnClickHelper($val, $tempKey);
            }
        } else {  // must be a literal value
            // escape newlines and carriage returns
            $search  = array("\n", "\r");
            $replace = array('\n', '\n');
            $escapedData = str_replace($search, $replace, $data);
            // am I forgetting any dangerous whitespace?
            // would a regex be faster?
            // if it's already encoded, don't encode it again
            if (!$this->_isEncoded($escapedData)) {
                $escapedData = urlencode($escapedData);
            }
            $escapedData = htmlentities($escapedData, ENT_QUOTES, 'UTF-8');

            $str .= 'input = document.createElement("input"); ';
            $str .= 'input.type = "hidden"; ';
            $str .= sprintf('input.name = "%s"; ', $prev);
            $str .= sprintf('input.value = "%s"; ', $escapedData);
            $str .= 'form.appendChild(input); ';
        }
        return $str;
    }

    // }}}
    // {{{ _isRegexp()

    /**
     * Returns true if the string is a regexp pattern
     *
     * @param string $string the pattern to check
     *
     * @return boolean
     * @access private
     */
    function _isRegexp($string) {
        return preg_match('/^\/.*\/([Uims]+)?$/', $string);
    }

    // }}}
    // {{{ _getLinksData()

    /**
     * Returns the correct link for the back/pages/next links
     *
     * @return array Data
     * @access private
     */
    function _getLinksData()
    {
        $qs = array();
        if ($this->_importQuery) {
            if ($this->_httpMethod == 'POST') {
                $qs = $_POST;
            } elseif ($this->_httpMethod == 'GET') {
                $qs = $_GET;
            }
        }
        foreach ($this->_excludeVars as $exclude) {
            $use_preg = $this->_isRegexp($exclude);
            foreach (array_keys($qs) as $qs_item) {
                if ($use_preg) {
                    if (preg_match($exclude, $qs_item, $matches)) {
                        foreach ($matches as $m) {
                            unset($qs[$m]);
                        }
                    }
                } elseif ($qs_item == $exclude) {
                    unset($qs[$qs_item]);
                    break;
                }
            }
        }
        if (count($this->_extraVars)) {
            $this->_recursive_urldecode($this->_extraVars);
            $qs = array_merge($qs, $this->_extraVars);
        }
        if (count($qs)
            && function_exists('get_magic_quotes_gpc')
            && -1 == version_compare(PHP_VERSION, '5.2.99')
            && get_magic_quotes_gpc()
        ) {
            $this->_recursive_stripslashes($qs);
        }
        return $qs;
    }

    // }}}
    // {{{ _recursive_stripslashes()

    /**
     * Helper method
     *
     * @param string|array &$var variable to clean
     *
     * @return void
     * @access private
     */
    function _recursive_stripslashes(&$var)
    {
        if (is_array($var)) {
            foreach (array_keys($var) as $k) {
                $this->_recursive_stripslashes($var[$k]);
            }
        } else {
            $var = stripslashes($var);
        }
    }

    // }}}
    // {{{ _recursive_urldecode()

    /**
     * Helper method
     *
     * @param string|array &$var variable to decode
     *
     * @return void
     * @access private
     */
    function _recursive_urldecode(&$var)
    {
        if (is_array($var)) {
            foreach (array_keys($var) as $k) {
                $this->_recursive_urldecode($var[$k]);
            }
        } else {
            $trans_tbl = array_flip(get_html_translation_table(HTML_ENTITIES));
            $var = strtr($var, $trans_tbl);
        }
    }

    // }}}
    // {{{ _getBackLink()

    /**
     * Returns back link
     *
     * @param string $url  URL to use in the link  [deprecated: use the factory instead]
     * @param string $link HTML to use as the link [deprecated: use the factory instead]
     *
     * @return string The link
     * @access private
     */
    function _getBackLink($url='', $link='')
    {
        //legacy settings... the preferred way to set an option
        //now is passing it to the factory
        if (!empty($url)) {
            $this->_path = $url;
        }
        if (!empty($link)) {
            $this->_prevImg = $link;
        }
        $back = '';
        if ($this->_currentPage > 1) {
            $this->_linkData[$this->_urlVar] = $this->getPreviousPageID();
            $back = $this->_renderLink($this->_altPrev, $this->_prevImg)
                  . $this->_spacesBefore . $this->_spacesAfter;
        } else if ($this->_prevImgEmpty !== null && $this->_totalPages > 1) {
            $back = $this->_prevImgEmpty
                  . $this->_spacesBefore . $this->_spacesAfter;
        }
        return $back;
    }

    // }}}
    // {{{ _getPageLinks()

    /**
     * Returns pages link
     *
     * @param string $url URL to use in the link [deprecated: use the factory instead]
     *
     * @return string Links
     * @access private
     */
    function _getPageLinks($url='')
    {
        $msg = 'function "_getPageLinks()" not implemented.';
        return $this->raiseError($msg, ERROR_PAGER_NOT_IMPLEMENTED);
    }

    // }}}
    // {{{ _getNextLink()

    /**
     * Returns next link
     *
     * @param string $url  URL to use in the link  [deprecated: use the factory instead]
     * @param string $link HTML to use as the link [deprecated: use the factory instead]
     *
     * @return string The link
     * @access private
     */
    function _getNextLink($url='', $link='')
    {
        //legacy settings... the preferred way to set an option
        //now is passing it to the factory
        if (!empty($url)) {
            $this->_path = $url;
        }
        if (!empty($link)) {
            $this->_nextImg = $link;
        }
        $next = '';
        if ($this->_currentPage < $this->_totalPages) {
            $this->_linkData[$this->_urlVar] = $this->getNextPageID();
            $next = $this->_spacesAfter
                  . $this->_renderLink($this->_altNext, $this->_nextImg)
                  . $this->_spacesBefore . $this->_spacesAfter;
        } else if ($this->_nextImgEmpty !== null && $this->_totalPages > 1) {
            $next = $this->_spacesAfter
                  . $this->_nextImgEmpty
                  . $this->_spacesBefore . $this->_spacesAfter;
        }
        return $next;
    }

    // }}}
    // {{{ _getFirstLinkTag()

    /**
     * Returns first link tag
     *
     * @param bool $raw should tag returned as array
     *
     * @return mixed string with html link tag or separated as array
     * @access private
     */
    function _getFirstLinkTag($raw = false)
    {
        if ($this->isFirstPage() || ($this->_httpMethod != 'GET')) {
            return $raw ? array() : '';
        }
        if ($raw) {
            return array(
                'url'   => $this->_getLinkTagUrl(1),
                'title' => $this->_firstLinkTitle
            );
        }
        return sprintf('<link rel="first" href="%s" title="%s" />'."\n",
            $this->_getLinkTagUrl(1),
            $this->_firstLinkTitle
        );
    }

    // }}}
    // {{{ _getPrevLinkTag()

    /**
     * Returns previous link tag
     *
     * @param bool $raw should tag returned as array
     *
     * @return mixed string with html link tag or separated as array
     * @access private
     */
    function _getPrevLinkTag($raw = false)
    {
        if ($this->isFirstPage() || ($this->_httpMethod != 'GET')) {
            return $raw ? array() : '';
        }
        if ($raw) {
            return array(
                'url'   => $this->_getLinkTagUrl($this->getPreviousPageID()),
                'title' => $this->_prevLinkTitle
            );
        }
        return sprintf('<link rel="previous" href="%s" title="%s" />'."\n",
            $this->_getLinkTagUrl($this->getPreviousPageID()),
            $this->_prevLinkTitle
        );
    }

    // }}}
    // {{{ _getNextLinkTag()

    /**
     * Returns next link tag
     *
     * @param bool $raw should tag returned as array
     *
     * @return mixed string with html link tag or separated as array
     * @access private
     */
    function _getNextLinkTag($raw = false)
    {
        if ($this->isLastPage() || ($this->_httpMethod != 'GET')) {
            return $raw ? array() : '';
        }
        if ($raw) {
            return array(
                'url'   => $this->_getLinkTagUrl($this->getNextPageID()),
                'title' => $this->_nextLinkTitle
            );
        }
        return sprintf('<link rel="next" href="%s" title="%s" />'."\n",
            $this->_getLinkTagUrl($this->getNextPageID()),
            $this->_nextLinkTitle
        );
    }

    // }}}
    // {{{ _getLastLinkTag()

    /**
     * Returns last link tag
     *
     * @param bool $raw should tag returned as array
     *
     * @return mixed string with html link tag or separated as array
     * @access private
     */
    function _getLastLinkTag($raw = false)
    {
        if ($this->isLastPage() || ($this->_httpMethod != 'GET')) {
            return $raw ? array() : '';
        }
        if ($raw) {
            return array(
                'url'   => $this->_getLinkTagUrl($this->_totalPages),
                'title' => $this->_lastLinkTitle
            );
        }
        return sprintf('<link rel="last" href="%s" title="%s" />'."\n",
            $this->_getLinkTagUrl($this->_totalPages),
            $this->_lastLinkTitle
        );
    }

    // }}}
    // {{{ _getLinkTagUrl()

    /**
     * Helper method
     *
     * @param integer $pageID page ID
     *
     * @return string the link tag url
     * @access private
     */
    function _getLinkTagUrl($pageID)
    {
        $this->_linkData[$this->_urlVar] = $pageID;
        if ($this->_append) {
            $href = '?' . $this->_http_build_query_wrapper($this->_linkData);
        } else {
            $href = str_replace('%d', $this->_linkData[$this->_urlVar], $this->_fileName);
        }
        return htmlentities($this->_url . $href, ENT_COMPAT, 'UTF-8');
    }

    // }}}
    // {{{ getPerPageSelectBox()

    /**
     * Returns a string with a XHTML SELECT menu,
     * useful for letting the user choose how many items per page should be
     * displayed. If parameter useSessions is TRUE, this value is stored in
     * a session var. The string isn't echoed right now so you can use it
     * with template engines.
     *
     * @param integer $start       starting value for the select menu
     * @param integer $end         ending value for the select menu
     * @param integer $step        step between values in the select menu
     * @param boolean $showAllData If true, perPage is set equal to totalItems.
     * @param array   $extraParams (or string $optionText for BC reasons)
     *                - 'optionText': text to show in each option.
     *                  Use '%d' where you want to see the number of pages selected.
     *                - 'attributes': (html attributes) Tag attributes or
     *                  HTML attributes (id="foo" pairs), will be inserted in the
     *                  <select> tag
     *
     * @return string xhtml select box
     * @access public
     */
    function getPerPageSelectBox($start=5, $end=30, $step=5, $showAllData=false, $extraParams=array())
    {
        include_once 'Pager/HtmlWidgets.php';
        $widget = new Pager_HtmlWidgets($this);
        return $widget->getPerPageSelectBox($start, $end, $step, $showAllData, $extraParams);
    }

    // }}}
    // {{{ getPageSelectBox()

    /**
     * Returns a string with a XHTML SELECT menu with the page numbers,
     * useful as an alternative to the links
     *
     * @param array  $params          - 'optionText': text to show in each option.
     *                                  Use '%d' where you want to see the number
     *                                  of pages selected.
     *                                - 'autoSubmit': if TRUE, add some js code
     *                                  to submit the form on the onChange event
     * @param string $extraAttributes (html attributes) Tag attributes or
     *                                HTML attributes (id="foo" pairs), will be
     *                                inserted in the <select> tag
     *
     * @return string xhtml select box
     * @access public
     */
    function getPageSelectBox($params = array(), $extraAttributes = '')
    {
        include_once 'Pager/HtmlWidgets.php';
        $widget = new Pager_HtmlWidgets($this);
        return $widget->getPageSelectBox($params, $extraAttributes);
    }

    // }}}
    // {{{ _printFirstPage()

    /**
     * Print [1]
     *
     * @return string String with link to 1st page,
     *                or empty string if this is the 1st page.
     * @access private
     */
    function _printFirstPage()
    {
        if ($this->isFirstPage()) {
            return '';
        }
        $this->_linkData[$this->_urlVar] = 1;
        return $this->_renderLink(
                str_replace('%d', 1, $this->_altFirst),
                $this->_firstPagePre . $this->_firstPageText . $this->_firstPagePost
        ) . $this->_spacesBefore . $this->_spacesAfter;
    }

    // }}}
    // {{{ _printLastPage()

    /**
     * Print [numPages()]
     *
     * @return string String with link to last page,
     *                or empty string if this is the 1st page.
     * @access private
     */
    function _printLastPage()
    {
        if ($this->isLastPage()) {
            return '';
        }
        $this->_linkData[$this->_urlVar] = $this->_totalPages;
        return $this->_renderLink(
                str_replace('%d', $this->_totalPages, $this->_altLast),
                $this->_lastPagePre . $this->_lastPageText . $this->_lastPagePost
        );
    }

    // }}}
    // {{{ _setFirstLastText()

    /**
     * sets the private _firstPageText, _lastPageText variables
     * based on whether they were set in the options
     *
     * @return void
     * @access private
     */
    function _setFirstLastText()
    {
        if ($this->_firstPageText == '') {
            $this->_firstPageText = '1';
        }
        if ($this->_lastPageText == '') {
            $this->_lastPageText = $this->_totalPages;
        }
    }

    // }}}
    // {{{ _http_build_query_wrapper()

    /**
     * This is a slightly modified version of the http_build_query() function;
     * it heavily borrows code from PHP_Compat's http_build_query().
     * The main change is the usage of htmlentities instead of urlencode,
     * since it's too aggressive
     *
     * @param array $data array of querystring values
     *
     * @return string
     * @access private
     */
    function _http_build_query_wrapper($data)
    {
        $data = (array)$data;
        if (empty($data)) {
            return '';
        }
        $separator = ini_get('arg_separator.output');
        if ($separator == '&amp;') {
            $separator = '&'; //the string is escaped by htmlentities anyway...
        }
        $tmp = array ();
        foreach ($data as $key => $val) {
            if (is_scalar($val)) {
                //array_push($tmp, $key.'='.$val);
                $val = urlencode($val);
                array_push($tmp, $key .'='. str_replace('%2F', '/', $val));
                continue;
            }
            // If the value is an array, recursively parse it
            if (is_array($val)) {
                array_push($tmp, $this->__http_build_query($val, urlencode($key)));
                continue;
            }
        }
        return implode($separator, $tmp);
    }

    // }}}
    // {{{ __http_build_query()

    /**
     * Helper function
     *
     * @param array  $array array of querystring values
     * @param string $name  key
     *
     * @return string
     * @access private
     */
    function __http_build_query($array, $name)
    {
        $tmp = array ();
        $separator = ini_get('arg_separator.output');
        if ($separator == '&amp;') {
            $separator = '&'; //the string is escaped by htmlentities anyway...
        }
        foreach ($array as $key => $value) {
            if (is_array($value)) {
                //array_push($tmp, $this->__http_build_query($value, sprintf('%s[%s]', $name, $key)));
                array_push($tmp, $this->__http_build_query($value, $name.'%5B'.$key.'%5D'));
            } elseif (is_scalar($value)) {
                //array_push($tmp, sprintf('%s[%s]=%s', $name, htmlentities($key), htmlentities($value)));
                array_push($tmp, $name.'%5B'.urlencode($key).'%5D='.urlencode($value));
            } elseif (is_object($value)) {
                //array_push($tmp, $this->__http_build_query(get_object_vars($value), sprintf('%s[%s]', $name, $key)));
                array_push($tmp, $this->__http_build_query(get_object_vars($value), $name.'%5B'.$key.'%5D'));
            }
        }
        return implode($separator, $tmp);
    }

    // }}}
    // {{{ _isEncoded()

    /**
     * Helper function
     * Check if a string is an encoded multibyte string
     *
     * @param string $string string to check
     *
     * @return boolean
     * @access private
     */

    function _isEncoded($string)
    {
        $hexchar = '&#[\dA-Fx]{2,};';
        return preg_match("/^(\s|($hexchar))*$/Uims", $string) ? true : false;
    }

    // }}}
    // {{{ raiseError()

    /**
     * conditionally includes PEAR base class and raise an error
     *
     * @param string  $msg  Error message
     * @param integer $code Error code
     *
     * @return PEAR_Error
     * @access private
     */
    function raiseError($msg, $code)
    {
        include_once 'PEAR.php';
        if (empty($this->_pearErrorMode)) {
            $this->_pearErrorMode = PEAR_ERROR_RETURN;
        }
        return PEAR::raiseError($msg, $code, $this->_pearErrorMode);
    }

    // }}}
    // {{{ setOptions()

    /**
     * Set and sanitize options
     *
     * @param mixed $options An associative array of option names and their values
     *
     * @return integer error code (PAGER_OK on success)
     * @access public
     */
    function setOptions($options)
    {
        foreach ($options as $key => $value) {
            if (in_array($key, $this->_allowed_options) && (!is_null($value))) {
                $this->{'_' . $key} = $value;
            }
        }

        //autodetect http method
        if (!isset($options['httpMethod'])
            && !isset($_GET[$this->_urlVar])
            && isset($_POST[$this->_urlVar])
        ) {
            $this->_httpMethod = 'POST';
        } else {
            $this->_httpMethod = strtoupper($this->_httpMethod);
        }

        if (substr($this->_path, -1, 1) == '/') {
            $this->_fileName = ltrim($this->_fileName, '/');  //strip leading slash
        }

        if ($this->_append) {
            if ($this->_fixFileName) {
                $this->_fileName = PAGER_CURRENT_FILENAME; //avoid possible user error;
            }
            $this->_url = $this->_path.(empty($this->_path) ? '' : '/').$this->_fileName;
        } else {
            $this->_url = $this->_path;
            if (0 != strncasecmp($this->_fileName, 'javascript', 10)) {
                $this->_url .= (empty($this->_path) ? '' : '/');
            }
            if (false === strpos($this->_fileName, '%d')) {
                trigger_error($this->errorMessage(ERROR_PAGER_INVALID_USAGE), E_USER_WARNING);
            }
        }
        if (substr($this->_url, 0, 2) == '//') {
            $this->_url = substr($this->_url, 1);
        }
        if (false === strpos($this->_altPage, '%d')) {
            //by default, append page number at the end
            $this->_altPage .= ' %d';
        }

        $this->_classString = '';
        if (strlen($this->_linkClass)) {
            $this->_classString = 'class="'.$this->_linkClass.'"';
        }

        if (strlen($this->_linkContainer)) {
            $this->_linkContainerPre .= empty($this->_curLinkContainerClassName) ? $this->_linkContainer : $this->_linkContainer . ' class="'.$this->_curLinkContainerClassName.'"';
        }

        if (strlen($this->_curTag)) {
            if (strlen($this->_curPageLinkClassName)) {
                $this->_curPageSpanPre = '<' . $this->_curTag . ' class="'.$this->_curPageLinkClassName.'">' . $this->_curPageSpanPre;
            } else {
                $this->_curPageSpanPre = '<' . $this->_curTag . '>' . $this->_curPageSpanPre;
            }
            $this->_curPageSpanPost = $this->_curPageSpanPost . '</' . $this->_curTag . '>';
        }

        $this->_perPage = max($this->_perPage, 1); //avoid possible user errors

        if ($this->_useSessions && !isset($_SESSION)) {
            session_start();
        }
        if (!empty($_REQUEST[$this->_sessionVar])) {
            $this->_perPage = max(1, (int)$_REQUEST[$this->_sessionVar]);
            if ($this->_useSessions) {
                $_SESSION[$this->_sessionVar] = $this->_perPage;
            }
        }

        if (!empty($_SESSION[$this->_sessionVar]) && $this->_useSessions) {
             $this->_perPage = $_SESSION[$this->_sessionVar];
        }

        if ($this->_closeSession) {
            session_write_close();
        }

        $this->_spacesBefore = str_repeat('&nbsp;', $this->_spacesBeforeSeparator);
        $this->_spacesAfter  = str_repeat('&nbsp;', $this->_spacesAfterSeparator);

        if (isset($_REQUEST[$this->_urlVar]) && empty($options['currentPage'])) {
            $this->_currentPage = (int)$_REQUEST[$this->_urlVar];
        }
        $this->_currentPage = max($this->_currentPage, 1);
        $this->_linkData = $this->_getLinksData();

        return PAGER_OK;
    }

    // }}}
    // {{{ getOption()

    /**
     * Return the current value of a given option
     *
     * @param string $name option name
     *
     * @return mixed option value
     * @access public
     */
    function getOption($name)
    {
        if (!in_array($name, $this->_allowed_options)) {
            $msg = 'invalid option: '.$name;
            return $this->raiseError($msg, ERROR_PAGER_INVALID);
        }
        return $this->{'_' . $name};
    }

    // }}}
    // {{{ getOptions()

    /**
     * Return an array with all the current pager options
     *
     * @return array list of all the pager options
     * @access public
     */
    function getOptions()
    {
        $options = array();
        foreach ($this->_allowed_options as $option) {
            $options[$option] = $this->{'_' . $option};
        }
        return $options;
    }

    // }}}
    // {{{ errorMessage()

    /**
     * Return a textual error message for a PAGER error code
     *
     * @param integer $code error code
     *
     * @return string error message
     * @access public
     */
    function errorMessage($code)
    {
        static $errorMessages;
        if (!isset($errorMessages)) {
            $errorMessages = array(
                ERROR_PAGER                     => 'unknown error',
                ERROR_PAGER_INVALID             => 'invalid',
                ERROR_PAGER_INVALID_PLACEHOLDER => 'invalid format - use "%d" as placeholder.',
                ERROR_PAGER_INVALID_USAGE       => 'if $options[\'append\'] is set to false, '
                                                  .' $options[\'fileName\'] MUST contain the "%d" placeholder.',
                ERROR_PAGER_NOT_IMPLEMENTED     => 'not implemented'
            );
        }

        return (isset($errorMessages[$code]) ?
            $errorMessages[$code] : $errorMessages[ERROR_PAGER]);
    }

    // }}}
}
?>