== 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
<?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"])) {
}
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;
}
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;
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,
}
$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>'
"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;
}
?>
--- /dev/null
+<?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;
+ }
+}
+?>
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;
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;
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");
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;
}
}
// 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 ) {
{
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' );