Revamped ajax interface, see release notes.
authorDaniel Kinzler <daniel@users.mediawiki.org>
Tue, 29 Aug 2006 15:43:34 +0000 (15:43 +0000)
committerDaniel Kinzler <daniel@users.mediawiki.org>
Tue, 29 Aug 2006 15:43:34 +0000 (15:43 +0000)
Note: wfSajaxSearch is broken (unrelated to these changes)

RELEASE-NOTES
includes/AjaxDispatcher.php
includes/AjaxFunctions.php
includes/AjaxResponse.php [new file with mode: 0644]
skins/common/ajax.js
skins/common/ajaxsearch.js

index c88fb51..5821628 100644 (file)
@@ -38,6 +38,11 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN
 
 == Changes since 1.7 ==
 
+* Introduced AjaxResponse object, superceding AjaxCachePolicy
+* Changes to sajax_do_call: optionally accept an element to fill instead of a
+  callback function; take the target function or element as a third parameter;
+  pass the full XMLHttpRequest object to the handler function, instead of just
+  the resultText value; use HTTP response codes to report errors.
 * (bug 6562) Removed unmaintained ParserXml.php for now
 * History paging overlap bug fixed
 * (bug 6586) Regression in "unblocked" subtitle
index 2704e27..ce10eec 100644 (file)
@@ -1,36 +1,23 @@
 <?php
 
-//$wgRequestTime = microtime();
-
-// unset( $IP );
-// @ini_set( 'allow_url_fopen', 0 ); # For security...
-
-# Valid web server entry point, enable includes.
-# Please don't move this line to includes/Defines.php. This line essentially defines
-# a valid entry point. If you put it in includes/Defines.php, then any script that includes
-# it becomes an entry point, thereby defeating its purpose.
-// define( 'MEDIAWIKI', true );
-// require_once( './includes/Defines.php' );
-// require_once( './LocalSettings.php' );
-// require_once( 'includes/Setup.php' );
-require_once( 'AjaxFunctions.php' );
+if( !defined( 'MEDIAWIKI' ) )
+        die( 1 );
 
 if ( ! $wgUseAjax ) {
        die( 1 );
 }
 
+require_once( 'AjaxFunctions.php' );
+require_once( 'AjaxResponse.php' );
+
 class AjaxDispatcher {
        var $mode;
        var $func_name;
        var $args;
 
        function AjaxDispatcher() {
-               global $wgAjaxCachePolicy;
-
                wfProfileIn( 'AjaxDispatcher::AjaxDispatcher' );
 
-               $wgAjaxCachePolicy = new AjaxCachePolicy();
-
                $this->mode = "";
 
                if (! empty($_GET["rs"])) {
@@ -60,21 +47,43 @@ class AjaxDispatcher {
        }
 
        function performAction() {
-               global $wgAjaxCachePolicy, $wgAjaxExportList, $wgOut;
+               global $wgAjaxExportList, $wgOut;
+               
                if ( empty( $this->mode ) ) {
                        return;
                }
                wfProfileIn( 'AjaxDispatcher::performAction' );
 
                if (! in_array( $this->func_name, $wgAjaxExportList ) ) {
-                       echo "-:{$this->func_name} not callable";
+                       header( 'Status: 400 Bad Request', true, 400 );
+                       echo "unknown function {$this->func_name}";
                } else {
-                       echo "+:";
-                       $result = call_user_func_array($this->func_name, $this->args);
-                       header( 'Content-Type: text/html; charset=utf-8', true );
-                       $wgAjaxCachePolicy->writeHeader();
-                       echo $result;
+                       try {
+                               $result = call_user_func_array($this->func_name, $this->args);
+                               
+                               if ( $result === false || $result === NULL ) {
+                                       header( 'Status: 500 Internal Error', true, 500 );
+                                       echo "{$this->func_name} returned no data";
+                               }
+                               else {
+                                       if ( is_string( $result ) ) {
+                                               $result= new AjaxResponse( $result );
+                                       }
+                                       
+                                       $result->sendHeaders();
+                                       $result->printText();
+                               }
+                               
+                       } catch (Exception $e) {
+                               if (!headers_sent()) {
+                                       header( 'Status: 500 Internal Error', true, 500 );
+                                       print $e->getMessage();
+                               } else {
+                                       print $e->getMessage();
+                               }
+                       }
                }
+               
                wfProfileOut( 'AjaxDispatcher::performAction' );
                $wgOut = null;
        }
index 9804a24..383dae4 100644 (file)
@@ -70,70 +70,8 @@ function code2utf($num){
    return '';
 }
 
-class AjaxCachePolicy {
-       var $policy;
-       var $vary;
-
-       function AjaxCachePolicy( $policy = null, $vary = null ) {
-               $this->policy = $policy;
-               $this->vary = $vary;
-       }
-
-       function setPolicy( $policy ) {
-               $this->policy = $policy;
-       }
-
-       function setVary( $vary ) {
-               $this->vary = $vary;
-       }
-
-       function writeHeader() {
-               global $wgUseSquid, $wgUseESI, $wgSquidMaxage;
-               
-               header ("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
-               
-               if ( $this->policy ) {
-                       
-                       # If squid caches are configured, tell them to cache the response, 
-                       # and tell the client to always check with the squid. Otherwise,
-                       # tell the client to use a cached copy, without a way to purge it.
-                       
-                       if( $wgUseSquid ) {
-                               
-                               # Expect explicite purge of the proxy cache, but require end user agents
-                               # to revalidate against the proxy on each visit.
-                               # Surrogate-Control controls our Squid, Cache-Control downstream caches
-                               
-                               if ( $wgUseESI ) {
-                                       header( 'Surrogate-Control: max-age='.$this->policy.', content="ESI/1.0"');
-                                       header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' );
-                               } else {
-                                       header( 'Cache-Control: s-maxage='.$this->policy.', must-revalidate, max-age=0' );
-                               }
-                               
-                       } else {
-                       
-                               # Let the client do the caching. Cache is not purged.
-                               header ("Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->policy ) . " GMT");
-                               header ("Cache-Control: s-max-age={$this->policy},public,max-age={$this->policy}");
-                       }
-                       
-               } else {
-                       # always expired, always modified
-                       header ("Expires: Mon, 26 Jul 1997 05:00:00 GMT");    // Date in the past
-                       header ("Cache-Control: no-cache, must-revalidate");  // HTTP/1.1
-                       header ("Pragma: no-cache");                          // HTTP/1.0
-               }
-               
-               if ( $this->vary ) {
-                       header ( "Vary: " . $this->vary );
-               }
-       }
-}
-                       
-
 function wfSajaxSearch( $term ) {
-       global $wgContLang, $wgAjaxCachePolicy, $wgOut;
+       global $wgContLang, $wgOut;
        $limit = 16;
        
        $l = new Linker;
@@ -145,8 +83,6 @@ function wfSajaxSearch( $term ) {
        if ( strlen( str_replace( '_', '', $term ) )<3 )
                return;
 
-       $wgAjaxCachePolicy->setPolicy( 30*60 );
-
        $db =& wfGetDB( DB_SLAVE );
        $res = $db->select( 'page', 'page_title',
                        array(  'page_namespace' => 0,
@@ -172,10 +108,10 @@ function wfSajaxSearch( $term ) {
        }
 
        $subtitlemsg = ( Title::newFromText($term) ? 'searchsubtitle' : 'searchsubtitleinvalid' );
-       $subtitle = $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) );
+       $subtitle = $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ); #FIXME: parser is missing mTitle !
 
        $term = htmlspecialchars( $term );
-       return '<div style="float:right; border:solid 1px black;background:gainsboro;padding:2px;"><a onclick="Searching_Hide_Results();">' 
+       $html = '<div style="float:right; border:solid 1px black;background:gainsboro;padding:2px;"><a onclick="Searching_Hide_Results();">' 
                . wfMsg( 'hideresults' ) . '</a></div>'
                . '<h1 class="firstHeading">'.wfMsg('search')
                . '</h1><div id="contentSub">'. $subtitle . '</div><ul><li>'
@@ -187,6 +123,12 @@ function wfSajaxSearch( $term ) {
                                        "search=$term&go=Go" )
                . "</li></ul><h2>" . wfMsg( 'articletitles', $term ) . "</h2>"
                . '<ul>' .$r .'</ul>'.$more;
+               
+       $response = new AjaxResponse( $html );
+       
+       $response->setCacheDuration( 30*60 );
+               
+       return $response;
 }
 
 ?>
diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php
new file mode 100644 (file)
index 0000000..40f5087
--- /dev/null
@@ -0,0 +1,203 @@
+<?php
+
+if( !defined( 'MEDIAWIKI' ) )
+        die( 1 );
+
+class AjaxResponse {
+       var $mCacheDuration;
+       var $mVary;
+       
+       var $mDisabled;
+       var $mText;
+       var $mResponseCode;
+       var $mLastModified;
+       var $mContentType;
+
+       function AjaxResponse( $text = NULL ) {
+               $this->mCacheDuration = NULL;
+               $this->mVary = NULL;
+               
+               $this->mDisabled = false;
+               $this->mText = '';
+               $this->mResponseCode = '200 OK';
+               $this->mLastModified = false;
+               $this->mContentType= 'text/html; charset=utf-8';
+               
+               if ( $text ) {
+                       $this->addText( $text );
+               }
+       }
+
+       function setCacheDuration( $duration ) {
+               $this->mCacheDuration = $duration;
+       }
+
+       function setVary( $vary ) {
+               $this->mVary = $vary;
+       }
+
+       function setResponseCode( $code ) {
+               $this->mResponseCode = $code;
+       }
+       
+       function setContentType( $type ) {
+               $this->mContentType = $type;
+       }
+       
+       function disable() {
+               $this->mDisabled = true;
+       }
+       
+       function addText( $text ) {
+               if ( ! $this->mDisabled && $text ) {
+                       $this->mText .= $text;
+               }
+       }
+
+       function printText() {
+               if ( ! $this->mDisabled ) {
+                       print $this->mText;
+               }
+       }
+       
+       function sendHeaders() {
+               global $wgUseSquid, $wgUseESI, $wgSquidMaxage;
+               
+               if ( $this->mResponseCode ) {
+                       $n = preg_replace( '/^ *(\d+)/', '\1', $this->mResponseCode );
+                       header( "Status: " . $this->mResponseCode, true, (int)$n );
+               }
+               
+               header ("Content-Type: " . $this->mContentType );
+               
+               if ( $this->mLastModified ) {
+                       header ("Last-Modified: " . $this->mLastModified );
+               }
+               else {
+                       header ("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
+               }
+               
+               if ( $this->mCacheDuration ) {
+                       
+                       # If squid caches are configured, tell them to cache the response, 
+                       # and tell the client to always check with the squid. Otherwise,
+                       # tell the client to use a cached copy, without a way to purge it.
+                       
+                       if( $wgUseSquid ) {
+                               
+                               # Expect explicite purge of the proxy cache, but require end user agents
+                               # to revalidate against the proxy on each visit.
+                               # Surrogate-Control controls our Squid, Cache-Control downstream caches
+                               
+                               if ( $wgUseESI ) {
+                                       header( 'Surrogate-Control: max-age='.$this->mCacheDuration.', content="ESI/1.0"');
+                                       header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' );
+                               } else {
+                                       header( 'Cache-Control: s-maxage='.$this->mCacheDuration.', must-revalidate, max-age=0' );
+                               }
+                               
+                       } else {
+                       
+                               # Let the client do the caching. Cache is not purged.
+                               header ("Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->mCacheDuration ) . " GMT");
+                               header ("Cache-Control: s-max-age={$this->mCacheDuration},public,max-age={$this->mCacheDuration}");
+                       }
+                       
+               } else {
+                       # always expired, always modified
+                       header ("Expires: Mon, 26 Jul 1997 05:00:00 GMT");    // Date in the past
+                       header ("Cache-Control: no-cache, must-revalidate");  // HTTP/1.1
+                       header ("Pragma: no-cache");                          // HTTP/1.0
+               }
+               
+               if ( $this->mVary ) {
+                       header ( "Vary: " . $this->mVary );
+               }
+       }
+       
+       /**
+        * checkLastModified tells the client to use the client-cached response if
+        * possible. If sucessful, the AjaxResponse is disabled so that
+        * any future call to AjaxResponse::printText() have no effect. The method
+        * returns true iff the response code was set to 304 Not Modified.
+        */
+       function checkLastModified ( $timestamp ) {
+               global $wgCachePages, $wgCacheEpoch, $wgUser, $wgRequest;
+               $fname = 'AjaxResponse::checkLastModified';
+
+               if ( !$timestamp || $timestamp == '19700101000000' ) {
+                       wfDebug( "$fname: CACHE DISABLED, NO TIMESTAMP\n" );
+                       return;
+               }
+               if( !$wgCachePages ) {
+                       wfDebug( "$fname: CACHE DISABLED\n", false );
+                       return;
+               }
+               if( $wgUser->getOption( 'nocache' ) ) {
+                       wfDebug( "$fname: USER DISABLED CACHE\n", false );
+                       return;
+               }
+
+               $timestamp = wfTimestamp( TS_MW, $timestamp );
+               $lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->mTouched, $wgCacheEpoch ) );
+
+               if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
+                       # IE sends sizes after the date like this:
+                       # Wed, 20 Aug 2003 06:51:19 GMT; length=5202
+                       # this breaks strtotime().
+                       $modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] );
+                       $modsinceTime = strtotime( $modsince );
+                       $ismodsince = wfTimestamp( TS_MW, $modsinceTime ? $modsinceTime : 1 );
+                       wfDebug( "$fname: -- client send If-Modified-Since: " . $modsince . "\n", false );
+                       wfDebug( "$fname: --  we might send Last-Modified : $lastmod\n", false );
+                       if( ($ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) {
+                               $this->setResponseCode( "304 Not Modified" );
+                               $this->disable();
+                               $this->mLastModified = $lastmod;
+                               
+                               wfDebug( "$fname: CACHED client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false );
+                               
+                               return true;
+                       } else {
+                               wfDebug( "$fname: READY  client: $ismodsince ; user: $wgUser->mTouched ; page: $timestamp ; site $wgCacheEpoch\n", false );
+                               $this->mLastModified = $lastmod;
+                       }
+               } else {
+                       wfDebug( "$fname: client did not send If-Modified-Since header\n", false );
+                       $this->mLastModified = $lastmod;
+               }
+       }
+       
+       function loadFromMemcached( $mckey, $touched ) {
+               global $wgMemc;
+               if ( !$touched ) return false;
+               
+               $mcvalue = $wgMemc->get( $mckey );
+               if ( $mcvalue ) {
+                       # Check to see if the value has been invalidated
+                       if ( $touched <= $mcvalue['timestamp'] ) {
+                               wfDebug( "Got $mckey from cache\n" );
+                               $this->mText = $mcvalue['value'];
+                               return true;
+                       } else {
+                               wfDebug( "$mckey has expired\n" );
+                       }
+               }
+               
+               return false;
+       }
+       
+       function storeInMemcached( $mckey, $expiry = 86400 ) {
+               global $wgMemc;
+               
+               $wgMemc->set( $mckey, 
+                       array(
+                               'timestamp' => wfTimestampNow(),
+                               'value' => $this->mText
+                       ), $expiry
+               );
+               
+               return true;
+       }
+}
+?>
index f9f9624..e9f4730 100644 (file)
@@ -3,11 +3,36 @@
 var sajax_debug_mode = false;
 var sajax_request_type = "GET";
 
+/**
+* if sajax_debug_mode is true, this function outputs given the message into 
+* the element with id = sajax_debug; if no such element exists in the document, 
+* it is injected.
+*/
 function sajax_debug(text) {
-       if (sajax_debug_mode)
-               alert("RSD: " + text)
+       if (!sajax_debug_mode) return;
+       
+       var e= document.getElementById('sajax_debug');
+       
+       if (!e) {
+               e= document.createElement("p");
+               e.className= 'sajax_debug';
+               e.id= 'sajax_debug';
+               
+               var b= document.getElementsByTagName("body")[0];
+               
+               if (b.firstChild) b.insertBefore(e, b.firstChild);
+               else b.appendChild(e);
+       }
+       
+       var m= document.createElement("div");
+       m.appendChild( document.createTextNode( text ) );
+       
+       e.appendChild( m );
 }
 
+/**
+* compatibility wrapper for creating a new XMLHttpRequest object. 
+*/
 function sajax_init_object() {
        sajax_debug("sajax_init_object() called..")
        var A;
@@ -27,8 +52,23 @@ function sajax_init_object() {
        return A;
 }
 
-
-function sajax_do_call(func_name, args) {
+/**
+* Perform an ajax call to mediawiki. Calls are handeled by AjaxDispatcher.php
+*   func_name - the name of the function to call. Must be registered in $wgAjaxExportList
+*   args - an array of arguments to that function
+*   target - the target that will handle the result of the call. If this is a function,
+*            if will be called with the XMLHttpRequest as a parameter; if it's an input
+*            element, its value will be set to the resultText; if it's another type of
+*            element, its innerHTML will be set to the resultText.
+*
+* Example:
+*    sajax_do_call('doFoo', [1, 2, 3], document.getElementById("showFoo"));
+*
+* This will call the doFoo function via MediaWiki's AjaxDispatcher, with
+* (1, 2, 3) as the parameter list, and will show the result in the element
+* with id = showFoo
+*/
+function sajax_do_call(func_name, args, target) {
        var i, x, n;
        var uri;
        var post_data;
@@ -38,16 +78,21 @@ function sajax_do_call(func_name, args) {
                        uri = uri + "?rs=" + escape(func_name);
                else
                        uri = uri + "&rs=" + escape(func_name);
-               for (i = 0; i < args.length-1; i++)
+               for (i = 0; i < args.length; i++)
                        uri = uri + "&rsargs[]=" + escape(args[i]);
                //uri = uri + "&rsrnd=" + new Date().getTime();
                post_data = null;
        } else {
                post_data = "rs=" + escape(func_name);
-               for (i = 0; i < args.length-1; i++)
+               for (i = 0; i < args.length; i++)
                        post_data = post_data + "&rsargs[]=" + escape(args[i]);
        }
        x = sajax_init_object();
+       if (!x) {
+               alert("AJAX not supported");
+               return false;
+       }
+       
        x.open(sajax_request_type, uri, true);
        if (sajax_request_type == "POST") {
                x.setRequestHeader("Method", "POST " + uri + " HTTP/1.1");
@@ -58,18 +103,33 @@ function sajax_do_call(func_name, args) {
        x.onreadystatechange = function() {
                if (x.readyState != 4)
                        return;
-               sajax_debug("received " + x.responseText);
-               var status;
-               var data;
-               status = x.responseText.charAt(0);
-               data = x.responseText.substring(2);
-               if (status == "-")
-                       alert("Error: " + data);
-               else
-                       args[args.length-1](data);
+                       
+               sajax_debug("received (" + x.status + " " + x.statusText + ") " + x.responseText);
+               
+               //if (x.status != 200)
+               //      alert("Error: " + x.status + " " + x.statusText + ": " + x.responseText);
+               //else
+               
+               if ( typeof( target ) == 'function' ) {
+                       target( x );
+               }
+               else if ( typeof( target ) == 'object' ) {
+                       if ( target.tagName == 'INPUT' ) {
+                               if (x.status == 200) target.value= x.responseText;
+                               //else alert("Error: " + x.status + " " + x.statusText + " (" + x.responseText + ")");
+                       }
+                       else {
+                               if (x.status == 200) target.innerHTML = x.responseText;
+                               else target.innerHTML= "<div class='error'>Error: " + x.status + " " + x.statusText + " (" + x.responseText + ")</div>";
+                       }
+               }
+               else {
+                       alert("bad target for sajax_do_call: not a function or object: " + target);
+               }
        }
+       
+       sajax_debug(func_name + " uri = " + uri + " / post = " + post_data);
        x.send(post_data);
-       sajax_debug(func_name + " uri = " + uri + "/post = " + post_data);
        sajax_debug(func_name + " waiting..");
        delete x;
 }
index f7973b7..e6ea31a 100644 (file)
@@ -38,8 +38,15 @@ function Search_Typing() {
 }
 
 // Set the body div to the results
-function Searching_SetResult(result)
+function Searching_SetResult( request )
 {
+       if ( request.status != 200 ) {
+               alert("Error: " + request.status + " " + request.statusText + ": " + request.responseText);
+               return;
+       }
+       
+       var result = request.responseText;
+
         //body.innerHTML = result;
        t = document.getElementById("searchTarget");
        if ( t == null ) {
@@ -83,15 +90,11 @@ function Searching_Call()
                {
                        return;
                }
-               x_wfSajaxSearch(x, Searching_SetResult);
+               
+               sajax_do_call( "wfSajaxSearch", [ x ], Searching_SetResult );
        }
 }
 
-function x_wfSajaxSearch() {
-       sajax_do_call( "wfSajaxSearch", x_wfSajaxSearch.arguments );
-}
-
-       
 //Initialize
 function sajax_onload() {
        x = document.getElementById( 'searchInput' );