From 6584cef20737bde8bb5776613da353722090e3dd Mon Sep 17 00:00:00 2001 From: Daniel Kinzler Date: Tue, 29 Aug 2006 15:43:34 +0000 Subject: [PATCH] Revamped ajax interface, see release notes. Note: wfSajaxSearch is broken (unrelated to these changes) --- RELEASE-NOTES | 5 + includes/AjaxDispatcher.php | 59 ++++++----- includes/AjaxFunctions.php | 76 ++------------ includes/AjaxResponse.php | 203 ++++++++++++++++++++++++++++++++++++ skins/common/ajax.js | 92 +++++++++++++--- skins/common/ajaxsearch.js | 17 +-- 6 files changed, 337 insertions(+), 115 deletions(-) create mode 100644 includes/AjaxResponse.php diff --git a/RELEASE-NOTES b/RELEASE-NOTES index c88fb51c9b..5821628841 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -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 diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index 2704e27dab..ce10eecd85 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -1,36 +1,23 @@ 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; } diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php index 9804a241ba..383dae462b 100644 --- a/includes/AjaxFunctions.php +++ b/includes/AjaxFunctions.php @@ -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 '
' + $html = '
' . wfMsg( 'hideresults' ) . '
' . '

'.wfMsg('search') . '

'. $subtitle . '

" . wfMsg( 'articletitles', $term ) . "

" . ''.$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 index 0000000000..40f508763a --- /dev/null +++ b/includes/AjaxResponse.php @@ -0,0 +1,203 @@ +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; + } +} +?> diff --git a/skins/common/ajax.js b/skins/common/ajax.js index f9f9624443..e9f4730459 100644 --- a/skins/common/ajax.js +++ b/skins/common/ajax.js @@ -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= "
Error: " + x.status + " " + x.statusText + " (" + x.responseText + ")
"; + } + } + 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; } diff --git a/skins/common/ajaxsearch.js b/skins/common/ajaxsearch.js index f7973b7d7a..e6ea31ab35 100644 --- a/skins/common/ajaxsearch.js +++ b/skins/common/ajaxsearch.js @@ -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' ); -- 2.20.1