home *** CD-ROM | disk | FTP | other *** search
- <?php
- /**
- * OO AJAX Implementation for PHP
- *
- * SVN Rev: $Id$
- *
- * @category HTML
- * @package AJAX
- * @author Joshua Eichorn <josh@bluga.net>
- * @copyright 2005 Joshua Eichorn
- * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
- * @version Release: 0.5.6
- */
-
- /**
- * Require the main AJAX library
- */
- require_once 'HTML/AJAX.php';
-
- /**
- * Class for creating an external AJAX server
- *
- * Can be used in 2 different modes, registerClass mode where you create an instance of the server and add the classes that will be registered
- * and then run handle request
- *
- * Or you can extend it and add init{className} methods for each class you want to export
- *
- * Client js generation is exposed through 2 _GET params client and stub
- * Setting the _GET param client to `all` will give you all the js classes needed
- * Setting the _GET param stub to `all` will give you stubs of all registered classes, you can also set it too just 1 class
- *
- * @category HTML
- * @package AJAX
- * @author Joshua Eichorn <josh@bluga.net>
- * @copyright 2005 Joshua Eichorn
- * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
- * @version Release: 0.5.6
- * @link http://pear.php.net/package/PackageName
- */
- class HTML_AJAX_Server
- {
-
- /**
- * Client options array if set to true the code looks at _GET
- * @var bool|array
- */
- var $options = true;
-
- /**
- * HTML_AJAX instance
- * @var HTML_AJAX
- */
- var $ajax;
-
- /**
- * Set to true if your extending the server to add init{className methods}
- * @var boolean
- * @access public
- */
- var $initMethods = false;
-
- /**
- * Location on filesystem of client javascript library
- * @var false|string if false the default pear data dir location is used
- */
- var $clientJsLocation = false;
-
- /**
- * An array of options that tell the server howto Cache output
- *
- * The rules are functions that make etag hash used to see if the client needs to download updated content
- * If you extend this class you can make your own rule function the naming convention is _cacheRule{RuleName}
- *
- * <code>
- * array(
- * 'httpCacheClient' => true, // send 304 headers for responses to ?client=* requests
- * 'ClientCacheRule' => 'File', // create a hash from file names and modified times, options: file|content
- * 'ClientCacheExpects'=> 'files', // what type of content to send to the hash function, options: files|classes|content
- * 'httpCacheStub' => true, // send 304 headers for responses to ?stub=* requests
- * 'StubCacheRule' => 'Api', // create a hash from the exposed api, options: api|content
- * 'StubCacheExpects'=> 'classes', // what type of content to send to the hash function, options: files|classes|content
- * )
- * </code>
- *
- * @var array
- * @access public
- */
- var $cacheOptions = array(
- 'httpCacheClient' => true,
- 'ClientCacheRule' => 'file',
- 'ClientCacheExpects' => 'files',
- 'httpCacheStub' => true,
- 'StubCacheRule' => 'api',
- 'StubCacheExpects' => 'classes',
- );
-
- /**
- * Compression Options
- *
- * <code>
- * array(
- * 'enabled' => false, // enable compression
- * 'type' => 'gzip' // the type of compression to do, options: gzip
- * )
- * </code>
- *
- * @var array
- * @access public
- */
- var $compression = array(
- 'enabled' => false,
- 'type' => 'gzip'
- );
-
- /**
- * Javascript library names and there path
- *
- * the return of $this->clientJsLocation(), is prepended before running readfile on them
- *
- * @access public
- * @var array
- */
- var $javascriptLibraries = array(
- 'all' => 'HTML_AJAX.js',
- 'html_ajax' => 'HTML_AJAX.js',
- 'html_ajax_lite'=> 'HTML_AJAX_lite.js',
- 'json' => 'serializer/JSON.js',
- 'request' => 'Request.js',
- 'main' => array('Compat.js','Main.js','clientPool.js'),
- 'httpclient' => 'HttpClient.js',
- 'dispatcher' => 'Dispatcher.js',
- 'util' => 'util.js',
- 'loading' => 'Loading.js',
- 'phpserializer' => 'serializer/phpSerializer.js',
- 'urlserializer' => 'serializer/UrlSerializer.js',
- 'haserializer' => 'serializer/haSerializer.js',
- 'clientpool' => 'clientPool.js',
- 'iframe' => 'IframeXHR.js',
- 'alias' => 'Alias.js',
- 'queues' => 'Queue.js',
- 'behavior' => array('behavior/behavior.js','behavior/cssQuery-p.js'),
-
- // rules to help you use a minimal library set
- 'standard' => array('Compat.js','clientPool.js','util.js','Main.js','HttpClient.js','Request.js','serializer/JSON.js',
- 'Loading.js','serializer/UrlSerializer.js','Alias.js','behavior/behavior.js','behavior/cssQuery-p.js'),
- 'jsonrpc' => array('Compat.js','util.js','Main.js','clientPool.js','HttpClient.js','Request.js','serializer/JSON.js'),
- 'proxyobjects' => array('Compat.js','util.js','Main.js','clientPool.js','Request.js','serializer/JSON.js','Dispatcher.js'),
-
- // BC rules
- 'priorityqueue' => 'Queue.js',
- 'orderedqueue' => 'Queue.js',
- );
-
- /**
- * Custom paths to use for javascript libraries, if not set {@link clientJsLocation} is used to find the system path
- *
- * @access public
- * @var array
- * @see registerJsLibrary
- */
- var $javascriptLibraryPaths = array();
-
- /**
- * Array of className => init methods to call, generated from constructor from initClassName methods
- *
- * @access protected
- */
- var $_initLookup = array();
-
-
- /**
- * Constructor creates the HTML_AJAX instance
- *
- * @param string $serverUrl (Optional) the url the client should be making a request too
- */
- function HTML_AJAX_Server($serverUrl = false)
- {
- $this->ajax = new HTML_AJAX();
-
- // parameters for HTML::AJAX
- $parameters = array('stub', 'client');
-
- // keep in the query string all the parameters that don't belong to AJAX
- // we remove all string like "parameter=something&". Final '&' can also
- // be '&' (to be sure) and is optional. '=something' is optional too.
- $querystring = '';
- if (isset($_SERVER['QUERY_STRING'])) {
- $querystring = preg_replace('/(' . join('|', $parameters) . ')(?:=[^&]*(?:&(?:amp;)?|$))?/', '', $this->ajax->_getServer('QUERY_STRING'));
- }
-
- // call the server with this query string
- if ($serverUrl === false) {
- $serverUrl = htmlentities($this->ajax->_getServer('PHP_SELF'));
- }
-
- if (substr($serverUrl,-1) != '?') {
- $serverUrl .= '?';
- }
- $this->ajax->serverUrl = $serverUrl . $querystring;
-
- $methods = get_class_methods($this);
- foreach($methods as $method) {
- if (preg_match('/^init([a-zA-Z0-9_]+)$/',$method,$match)) {
- $this->_initLookup[strtolower($match[1])] = $method;
- }
- }
- }
-
- /**
- * Handle a client request, either generating a client or having HTML_AJAX handle the request
- *
- * @return boolean true if request was handled, false otherwise
- */
- function handleRequest()
- {
- if ($this->options == true) {
- $this->_loadOptions();
- }
- //basically a hook for iframe but allows processing of data earlier
- $this->ajax->populatePayload();
- if (!isset($_GET['c']) && (count($this->options['client']) > 0 || count($this->options['stub']) > 0) ) {
- $this->generateClient();
- return true;
- } else {
- if (!empty($_GET['c'])) {
- $this->_init($this->_cleanIdentifier($this->ajax->_getVar('c')));
- }
- return $this->ajax->handleRequest();
- }
- }
-
- /**
- * Register method passthrough to HTML_AJAX
- *
- * @see HTML_AJAX::registerClass for docs
- */
- function registerClass(&$instance, $exportedName = false, $exportedMethods = false)
- {
- $this->ajax->registerClass($instance,$exportedName,$exportedMethods);
- }
-
- /**
- * Change default serialization - important for exporting classes
- *
- * I wanted this for the xml serializer :)
- */
- function setSerializer($type)
- {
- $this->ajax->serializer = $type;
- $this->ajax->unserializer = $type;
- }
-
- /**
- * Register a new js client library
- *
- * @param string $libraryName name you'll reference the library as
- * @param string|array $fileName actual filename with no path, for example customLib.js
- * @param string|false $path Optional, if not set the result from jsClientLocation is used
- */
- function registerJSLibrary($libraryName,$fileName,$path = false) {
- $libraryName = strtolower($libraryName);
- $this->javascriptLibraries[$libraryName] = $fileName;
-
- if ($path !== false) {
- $this->javascriptLibraryPaths[$libraryName] = $path;
- }
- }
-
- /**
- * Register init methods from an external class
- *
- * @param object $instance an external class with initClassName methods
- */
- function registerInitObject(&$instance) {
- $instance->server =& $this;
- $methods = get_class_methods($instance);
- foreach($methods as $method) {
- if (preg_match('/^init([a-zA-Z0-9_]+)$/',$method,$match)) {
- $this->_initLookup[strtolower($match[1])] = array(&$instance,$method);
- }
- }
- }
-
- /**
- * Register a callback to be exported to the client
- *
- * This function uses the PHP callback pseudo-type
- *
- */
- function registerPhpCallback($callback)
- {
- if (!is_callable($callback)) {
- // invalid callback
- return false;
- }
-
- if (is_array($callback) && is_object($callback[0])) {
- // object method
- $this->registerClass($callback[0], strtolower(get_class($callback[0])), array($callback[1]));
- return true;
- }
-
- // static callback
- $this->ajax->registerPhpCallback($callback);
- }
-
- /**
- * Generate client js
- *
- * @todo this is going to need tests to cover all the options
- */
- function generateClient()
- {
- $headers = array();
-
- ob_start();
-
- // create a list list of js files were going to need to output
- // index is the full file and so is the value, this keeps duplicates out of $fileList
- $fileList = array();
-
- if(!is_array($this->options['client'])) {
- $this->options['client'] = array();
- }
- foreach($this->options['client'] as $library) {
- if (isset($this->javascriptLibraries[$library])) {
- $lib = (array)$this->javascriptLibraries[$library];
- foreach($lib as $file) {
- if (isset($this->javascriptLibraryPaths[$library])) {
- $fileList[$this->javascriptLibraryPaths[$library].$file] = $this->javascriptLibraryPaths[$library].$file;
- }
- else {
- $fileList[$this->clientJsLocation().$file] = $this->clientJsLocation().$file;
- }
- }
- }
- }
-
- // do needed class init if were running an init server
- if(!is_array($this->options['stub'])) {
- $this->options['stub'] = array();
- }
- $classList = $this->options['stub'];
- if ($this->initMethods) {
- if (isset($this->options['stub'][0]) && $this->options['stub'][0] === 'all') {
- $this->_initAll();
- } else {
- foreach($this->options['stub'] as $stub) {
- $this->_init($stub);
- }
- }
- }
- if (isset($this->options['stub'][0]) && $this->options['stub'][0] === 'all') {
- $classList = array_keys($this->ajax->_exportedInstances);
- }
-
- // if were doing stub and client we have to wait for both ETags before we can compare with the client
- $combinedOutput = false;
- if ($classList != false && count($classList) > 0 && count($fileList) > 0) {
- $combinedOutput = true;
- }
-
-
- if ($classList != false && count($classList) > 0) {
-
- // were setup enough to make a stubETag if the input it wants is a class list
- if ($this->cacheOptions['httpCacheStub'] &&
- $this->cacheOptions['StubCacheExpects'] == 'classes')
- {
- $stubETag = $this->_callCacheRule('Stub',$classList);
- }
-
- // if were not in combined output compare etags, if method returns true were done
- if (!$combinedOutput && isset($stubETag)) {
- if ($this->_compareEtags($stubETag)) {
- ob_end_clean();
- return;
- }
- }
-
- // output the stubs for all the classes in our list
- foreach($classList as $class) {
- echo $this->ajax->generateClassStub($class);
- }
-
- // if were cacheing and the rule expects content make a tag and check it, if the check is true were done
- if ($this->cacheOptions['httpCacheStub'] &&
- $this->cacheOptions['StubCacheExpects'] == 'content')
- {
- $stubETag = $this->_callCacheRule('Stub',ob_get_contents());
- }
-
- // if were not in combined output compare etags, if method returns true were done
- if (!$combinedOutput && isset($stubETag)) {
- if ($this->_compareEtags($stubETag)) {
- ob_end_clean();
- return;
- }
- }
- }
-
- if (count($fileList) > 0) {
- // if were caching and need a file list build our jsETag
- if ($this->cacheOptions['httpCacheClient'] &&
- $this->cacheOptions['ClientCacheExpects'] === 'files')
- {
- $jsETag = $this->_callCacheRule('Client',$fileList);
-
- }
-
- // if were not in combined output compare etags, if method returns true were done
- if (!$combinedOutput && isset($jsETag)) {
- if ($this->_compareEtags($jsETag)) {
- ob_end_clean();
- return;
- }
- }
-
- // output the needed client js files
- foreach($fileList as $file) {
- $this->_readFile($file);
- }
-
- // if were caching and need content build the etag
- if ($this->cacheOptions['httpCacheClient'] &&
- $this->cacheOptions['ClientCacheExpects'] === 'content')
- {
- $jsETag = $this->_callCacheRule('Client',ob_get_contents());
- }
-
- // if were not in combined output compare etags, if method returns true were done
- if (!$combinedOutput && isset($jsETag)) {
- if ($this->_compareEtags($jsETag)) {
- ob_end_clean();
- return;
- }
- }
- // were in combined output, merge the 2 ETags and compare
- else if (isset($jsETag) && isset($stubETag)) {
- if ($this->_compareEtags(md5($stubETag.$jsETag))) {
- ob_end_clean();
- return;
- }
- }
- }
-
-
- // were outputting content, add our length header and send the output
- $length = ob_get_length();
- $output = ob_get_contents();
- ob_end_clean();
-
- if ($this->ajax->packJavaScript) {
- $output = $this->ajax->packJavaScript($output);
- $length = strlen($output);
- }
-
- if ($this->compression['enabled'] && $this->compression['type'] == 'gzip' && strpos($_SERVER["HTTP_ACCEPT_ENCODING"], "gzip") !== false) {
- $output = gzencode($output,9);
- $length = strlen($output);
- $headers['Content-Encoding'] = 'gzip';
- }
-
- if ($length > 0 && $this->ajax->_sendContentLength()) {
- $headers['Content-Length'] = $length;
- }
- $headers['Content-Type'] = 'text/javascript; charset=utf-8';
- $this->ajax->_sendHeaders($headers);
- echo($output);
- }
-
- /**
- * Run readfile on input with basic error checking
- *
- * @param string $file file to read
- * @access private
- * @todo is addslashes enough encoding for js?
- */
- function _readFile($file)
- {
- if (file_exists($file)) {
- readfile($file);
- } else {
- $file = addslashes($file);
- echo "alert('Unable to find javascript file: $file');";
- }
- }
-
- /**
- * Get the location of the client js
- * To override the default pear datadir location set $this->clientJsLocation
- *
- * @return string
- */
- function clientJsLocation()
- {
- if (!$this->clientJsLocation) {
- $path = 'C:\php5\pear\data'.DIRECTORY_SEPARATOR.'HTML_AJAX'.DIRECTORY_SEPARATOR.'js'.DIRECTORY_SEPARATOR;
- if(strpos($path, '@'.'data-dir@') === 0)
- {
- $path = realpath(dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'js').DIRECTORY_SEPARATOR;
- }
- return $path;
- } else {
- return $this->clientJsLocation;
- }
- }
-
- /**
- * Set the location of the client js
- *
- * @access public
- * @param string $location Location
- * @return void
- */
- function setClientJsLocation($location)
- {
- $this->clientJsLocation = $location;
- }
-
- /**
- * Set the path to a Javascript libraries
- *
- * @access public
- * @param string $library Library name
- * @param string $path Path
- * @return void
- */
- function setJavascriptLibraryPath($library, $path)
- {
- $this->javascriptLibraryPaths[$library] = $path;
- }
-
- /**
- * Set the path to more than one Javascript libraries at once
- *
- * @access public
- * @param array $paths Paths
- * @return void
- */
- function setJavascriptLibraryPaths($paths)
- {
- if (is_array($paths)) {
- $this->javascriptLibraryPaths = array_merge($this->javascriptLibraryPaths, $paths);
- }
- }
-
- /**
- * Load options from _GET
- *
- * @access private
- */
- function _loadOptions()
- {
- $this->options = array('client'=>array(),'stub'=>array());
- if (isset($_GET['client'])) {
- $clients = explode(',',$this->ajax->_getVar('client'));
- $client = array();
- foreach($clients as $val) {
- $cleanVal = $this->_cleanIdentifier($val);
- if (!empty($cleanVal)) {
- $client[] = strtolower($cleanVal);
- }
- }
-
- if (count($client) > 0) {
- $this->options['client'] = $client;
- }
- }
- if (isset($_GET['stub'])) {
- $stubs = explode(',',$this->ajax->_getVar('stub'));
- $stub = array();
- foreach($stubs as $val) {
- $cleanVal = $this->_cleanIdentifier($val);
- if (!empty($cleanVal)) {
- $stub[] = strtolower($cleanVal);
- }
- }
-
- if (count($stub) > 0) {
- $this->options['stub'] = $stub;
- }
- }
- }
-
- /**
- * Clean an identifier like a class name making it safe to use
- *
- * @param string $input
- * @return string
- * @access private
- */
- function _cleanIdentifier($input) {
- return trim(preg_replace('/[^A-Za-z_0-9]/','',$input));
- }
-
- /**
- * Run every init method on the class
- *
- * @access private
- */
- function _initAll()
- {
- if ($this->initMethods) {
- foreach($this->_initLookup as $class => $method) {
- $this->_init($class);
- }
- }
- }
-
- /**
- * Init one class
- *
- * @param string $className
- * @access private
- */
- function _init($className)
- {
- $className = strtolower($className);
- if ($this->initMethods) {
- if (isset($this->_initLookup[$className])) {
- $method =& $this->_initLookup[$className];
- if (is_array($method)) {
- call_user_func($method);
- }
- else {
- $this->$method();
- }
- } else {
- trigger_error("Could find an init method for class: " . $className);
- }
- }
- }
-
- /**
- * Generate a hash from a list of files
- *
- * @param array $files file list
- * @return string a hash that can be used as an etag
- * @access private
- */
- function _cacheRuleFile($files) {
- $signature = "";
- foreach($files as $file) {
- if (file_exists($file)) {
- $signature .= $file.filemtime($file);
- }
- }
- return md5($signature);
- }
-
- /**
- * Generate a hash from the api of registered classes
- *
- * @param array $classes class list
- * @return string a hash that can be used as an etag
- * @access private
- */
- function _cacheRuleApi($classes) {
- $signature = "";
- foreach($classes as $class) {
- if (isset($this->ajax->_exportedInstances[$class])) {
- $signature .= $class.implode(',',$this->ajax->_exportedInstances[$class]['exportedMethods']);
- }
- }
- return md5($signature);
- }
-
- /**
- * Generate a hash from the raw content
- *
- * @param array $content
- * @return string a hash that can be used as an etag
- * @access private
- */
- function _cacheRuleContent($content) {
- return md5($content);
- }
-
- /**
- * Send cache control headers
- * @access private
- */
- function _sendCacheHeaders($etag,$notModified) {
- header('Cache-Control: must-revalidate');
- header('ETag: '.$etag);
- if ($notModified) {
- header('HTTP/1.0 304 Not Modified',false,304);
- }
- }
-
- /**
- * Compare eTags
- *
- * @param string $serverETag server eTag
- * @return boolean
- * @access private
- */
- function _compareEtags($serverETag) {
- if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
- if (strcmp($this->ajax->_getServer('HTTP_IF_NONE_MATCH'),$serverETag) == 0) {
- $this->_sendCacheHeaders($serverETag,true);
- return true;
- }
- }
- $this->_sendCacheHeaders($serverETag,false);
- return false;
- }
-
- /**
- * Call a cache rule and return its retusn
- *
- * @param string $rule Stub|Client
- * @param mixed $payload
- * @return boolean
- * @access private
- * @todo decide if error checking is needed
- */
- function _callCacheRule($rule,$payload) {
- $method = '_cacheRule'.$this->cacheOptions[$rule.'CacheRule'];
- return call_user_func(array(&$this,$method),$payload);
- }
- }
- /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
- ?>
-