'ApiRevisionDelete' => 'includes/api/ApiRevisionDelete.php',
'ApiRollback' => 'includes/api/ApiRollback.php',
'ApiRsd' => 'includes/api/ApiRsd.php',
- 'ApiRunJobs' => 'includes/api/ApiRunJobs.php',
'ApiSetNotificationTimestamp' => 'includes/api/ApiSetNotificationTimestamp.php',
'ApiTokens' => 'includes/api/ApiTokens.php',
'ApiUnblock' => 'includes/api/ApiUnblock.php',
'SpecialRedirect' => 'includes/specials/SpecialRedirect.php',
'SpecialResetTokens' => 'includes/specials/SpecialResetTokens.php',
'SpecialRevisionDelete' => 'includes/specials/SpecialRevisiondelete.php',
+ 'SpecialRunJobs' => 'includes/specials/SpecialRunJobs.php',
'SpecialSearch' => 'includes/specials/SpecialSearch.php',
'SpecialSpecialpages' => 'includes/specials/SpecialSpecialpages.php',
'SpecialStatistics' => 'includes/specials/SpecialStatistics.php',
* the socket once it's done.
*/
protected function triggerJobs() {
- global $wgJobRunRate, $wgServer, $wgScriptPath, $wgScriptExtension, $wgEnableAPI;
+ global $wgJobRunRate, $wgServer;
if ( $wgJobRunRate <= 0 || wfReadOnly() ) {
return;
+ } elseif ( $this->getTitle()->isSpecial( 'RunJobs' ) ) {
+ return; // recursion guard
}
$section = new ProfileSection( __METHOD__ );
$n = intval( $wgJobRunRate );
}
- $query = array( 'action' => 'runjobs',
+ $query = array( 'title' => 'Special:RunJobs',
'tasks' => 'jobs', 'maxjobs' => $n, 'sigexpiry' => time() + 5 );
- $query['signature'] = ApiRunJobs::getQuerySignature( $query );
-
- if ( !$wgEnableAPI ) {
- // Fall back to running the job here while the user waits
- ApiRunJobs::executeJobs( $n );
- return;
- }
+ $query['signature'] = SpecialRunJobs::getQuerySignature( $query );
$errno = $errstr = null;
$info = wfParseUrl( $wgServer );
if ( !$sock ) {
wfDebugLog( 'runJobs', "Failed to start cron API (socket error $errno): $errstr\n" );
// Fall back to running the job here while the user waits
- ApiRunJobs::executeJobs( $n );
+ SpecialRunJobs::executeJobs( $n );
return;
}
- $url = wfAppendQuery( "{$wgScriptPath}/api{$wgScriptExtension}", $query );
+ $url = wfAppendQuery( wfScript( 'index' ), $query );
$req = "POST $url HTTP/1.1\r\nHost: {$info['host']}\r\nConnection: Close\r\n\r\n";
wfDebugLog( 'runJobs', "Running $n job(s) via '$url'\n" );
'purge' => 'ApiPurge',
'setnotificationtimestamp' => 'ApiSetNotificationTimestamp',
'rollback' => 'ApiRollback',
- 'runjobs' => 'ApiRunJobs',
'delete' => 'ApiDelete',
'undelete' => 'ApiUndelete',
'protect' => 'ApiProtect',
+++ /dev/null
-<?php
-/**
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author Aaron Schulz
- */
-
-/**
- * This is a simple class to handle action=runjobs and is only used internally
- *
- * @note: this does not requre "write mode" nor tokens due to the signature check
- *
- * @ingroup API
- */
-class ApiRunJobs extends ApiBase {
- public function execute() {
- if ( wfReadOnly() ) {
- $this->dieUsage( 'Wiki is in read-only mode', 'read_only', 400 );
- }
-
- $params = $this->extractRequestParams();
- $squery = $this->getRequest()->getValues();
- unset( $squery['signature'] );
- $cSig = self::getQuerySignature( $squery );
- $rSig = $params['signature'];
-
- // Time-insensitive signature verification
- if ( strlen( $rSig ) !== strlen( $cSig ) ) {
- $verified = false;
- } else {
- $result = 0;
- for ( $i = 0; $i < strlen( $cSig ); $i++ ) {
- $result |= ord( $cSig{$i} ) ^ ord( $rSig{$i} );
- }
- $verified = ( $result == 0 );
- }
-
- if ( !$verified || $params['sigexpiry'] < time() ) {
- $this->dieUsage( 'Invalid or stale signature provided', 'bad_signature', 400 );
- }
-
- // Client will usually disconnect before checking the response,
- // but it needs to know when it is safe to disconnect. Until this
- // reaches ignore_user_abort(), it is not safe as the jobs won't run.
- ignore_user_abort( true ); // jobs may take a bit of time
- header( "HTTP/1.0 202 Accepted" );
- ob_flush();
- flush();
- // Once the client receives this response, it can disconnect
-
- // Do all of the specified tasks...
- if ( in_array( 'jobs', $params['tasks'] ) ) {
- self::executeJobs( $params['maxjobs'] );
- }
- }
-
- /**
- * @param array $query
- * @return string
- */
- public static function getQuerySignature( array $query ) {
- global $wgSecretKey;
-
- ksort( $query ); // stable order
- return hash_hmac( 'sha1', wfArrayToCgi( $query ), $wgSecretKey );
- }
-
- /**
- * Run jobs from the job queue
- *
- * @note: also called from Wiki.php
- *
- * @param integer $maxJobs Maximum number of jobs to run
- * @return void
- */
- public static function executeJobs( $maxJobs ) {
- $n = $maxJobs; // number of jobs to run
- if ( $n < 1 ) {
- return;
- }
- try {
- $group = JobQueueGroup::singleton();
- $count = $group->executeReadyPeriodicTasks();
- if ( $count > 0 ) {
- wfDebugLog( 'jobqueue', "Executed $count periodic queue task(s)." );
- }
-
- do {
- $job = $group->pop( JobQueueGroup::TYPE_DEFAULT, JobQueueGroup::USE_CACHE ); // job from any queue
- if ( $job ) {
- $output = $job->toString() . "\n";
- $t = - microtime( true );
- wfProfileIn( __METHOD__ . '-' . get_class( $job ) );
- $success = $job->run();
- wfProfileOut( __METHOD__ . '-' . get_class( $job ) );
- $group->ack( $job ); // done
- $t += microtime( true );
- $t = round( $t * 1000 );
- if ( $success === false ) {
- $output .= "Error: " . $job->getLastError() . ", Time: $t ms\n";
- } else {
- $output .= "Success, Time: $t ms\n";
- }
- wfDebugLog( 'jobqueue', $output );
- }
- } while ( --$n && $job );
- } catch ( MWException $e ) {
- // We don't want exceptions thrown during job execution to
- // be reported to the user since the output is already sent.
- // Instead we just log them.
- MWExceptionHandler::logException( $e );
- }
- }
-
- public function mustBePosted() {
- return true;
- }
-
- public function getAllowedParams() {
- return array(
- 'tasks' => array(
- ApiBase::PARAM_ISMULTI => true,
- ApiBase::PARAM_TYPE => array( 'jobs' )
- ),
- 'maxjobs' => array(
- ApiBase::PARAM_TYPE => 'integer',
- ApiBase::PARAM_DFLT => 0
- ),
- 'signature' => array(
- ApiBase::PROP_TYPE => 'string',
- ),
- 'sigexpiry' => array(
- ApiBase::PARAM_TYPE => 'integer',
- ApiBase::PARAM_DFLT => 0 // ~epoch
- ),
- );
- }
-
- public function getParamDescription() {
- return array(
- 'tasks' => 'List of task types to perform',
- 'maxjobs' => 'Maximum number of jobs to run',
- 'signature' => 'HMAC Signature that signs the request',
- 'sigexpiry' => 'HMAC signature expiry as a UNIX timestamp'
- );
- }
-
- public function getDescription() {
- return 'Perform periodic tasks or run jobs from the queue.';
- }
-
- public function getExamples() {
- return array(
- 'api.php?action=runjobs&tasks=jobs&maxjobs=3' => 'Run up to 3 jobs from the queue',
- );
- }
-}
'PermanentLink' => 'SpecialPermanentLink',
'Redirect' => 'SpecialRedirect',
'Revisiondelete' => 'SpecialRevisionDelete',
+ 'RunJobs' => 'SpecialRunJobs',
'Specialpages' => 'SpecialSpecialpages',
'Userlogout' => 'SpecialUserlogout',
);
--- /dev/null
+<?php
+/**
+ * Implements Special:RunJobs
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ * @author Aaron Schulz
+ */
+
+/**
+ * Special page designed for running background tasks (internal use only)
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialRunJobs extends UnlistedSpecialPage {
+ public function __construct() {
+ parent::__construct( 'RunJobs' );
+ }
+
+ public function execute( $par = '' ) {
+ $this->getOutput()->disable();
+
+ if ( wfReadOnly() ) {
+ header( "HTTP/1.0 423 Locked" );
+ print 'Wiki is in read-only mode';
+ return;
+ } elseif ( !$this->getRequest()->wasPosted() ) {
+ header( "HTTP/1.0 400 Bad Request" );
+ print 'Request must be POSTed';
+ return;
+ }
+
+ $optional = array( 'maxjobs' => 0 );
+ $required = array_flip( array( 'title', 'tasks', 'signature', 'sigexpiry' ) );
+
+ $params = array_intersect_key( $this->getRequest()->getValues(), $required + $optional );
+ $missing = array_diff_key( $required, $params );
+ if ( count( $missing ) ) {
+ header( "HTTP/1.0 400 Bad Request" );
+ print 'Missing parameters: ' . implode( ', ', array_keys( $missing ) );
+ return;
+ }
+
+ $squery = $params;
+ unset( $squery['signature'] );
+ $cSig = self::getQuerySignature( $squery ); // correct signature
+ $rSig = $params['signature']; // provided signature
+
+ // Constant-time signature verification
+ // http://www.emerose.com/timing-attacks-explained
+ // @todo: make a common method for this
+ if ( !is_string( $rSig ) || strlen( $rSig ) !== strlen( $cSig ) ) {
+ $verified = false;
+ } else {
+ $result = 0;
+ for ( $i = 0; $i < strlen( $cSig ); $i++ ) {
+ $result |= ord( $cSig{$i} ) ^ ord( $rSig{$i} );
+ }
+ $verified = ( $result == 0 );
+ }
+ if ( !$verified || $params['sigexpiry'] < time() ) {
+ header( "HTTP/1.0 400 Bad Request" );
+ print 'Invalid or stale signature provided';
+ return;
+ }
+
+ // Apply any default parameter values
+ $params += $optional;
+
+ // Client will usually disconnect before checking the response,
+ // but it needs to know when it is safe to disconnect. Until this
+ // reaches ignore_user_abort(), it is not safe as the jobs won't run.
+ ignore_user_abort( true ); // jobs may take a bit of time
+ header( "HTTP/1.0 202 Accepted" );
+ ob_flush();
+ flush();
+ // Once the client receives this response, it can disconnect
+
+ // Do all of the specified tasks...
+ if ( in_array( 'jobs', explode( '|', $params['tasks'] ) ) ) {
+ self::executeJobs( (int)$params['maxjobs'] );
+ }
+ }
+
+ /**
+ * @param array $query
+ * @return string
+ */
+ public static function getQuerySignature( array $query ) {
+ global $wgSecretKey;
+
+ ksort( $query ); // stable order
+ return hash_hmac( 'sha1', wfArrayToCgi( $query ), $wgSecretKey );
+ }
+
+ /**
+ * Run jobs from the job queue
+ *
+ * @note: also called from Wiki.php
+ *
+ * @param integer $maxJobs Maximum number of jobs to run
+ * @return void
+ */
+ public static function executeJobs( $maxJobs ) {
+ $n = $maxJobs; // number of jobs to run
+ if ( $n < 1 ) {
+ return;
+ }
+ try {
+ $group = JobQueueGroup::singleton();
+ $count = $group->executeReadyPeriodicTasks();
+ if ( $count > 0 ) {
+ wfDebugLog( 'jobqueue', "Executed $count periodic queue task(s)." );
+ }
+
+ do {
+ $job = $group->pop( JobQueueGroup::TYPE_DEFAULT, JobQueueGroup::USE_CACHE );
+ if ( $job ) {
+ $output = $job->toString() . "\n";
+ $t = - microtime( true );
+ wfProfileIn( __METHOD__ . '-' . get_class( $job ) );
+ $success = $job->run();
+ wfProfileOut( __METHOD__ . '-' . get_class( $job ) );
+ $group->ack( $job ); // done
+ $t += microtime( true );
+ $t = round( $t * 1000 );
+ if ( $success === false ) {
+ $output .= "Error: " . $job->getLastError() . ", Time: $t ms\n";
+ } else {
+ $output .= "Success, Time: $t ms\n";
+ }
+ wfDebugLog( 'jobqueue', $output );
+ }
+ } while ( --$n && $job );
+ } catch ( MWException $e ) {
+ // We don't want exceptions thrown during job execution to
+ // be reported to the user since the output is already sent.
+ // Instead we just log them.
+ MWExceptionHandler::logException( $e );
+ }
+ }
+}
'Redirect' => array( 'Redirect' ),
'ResetTokens' => array( 'ResetTokens' ),
'Revisiondelete' => array( 'RevisionDelete' ),
+ 'RunJobs' => array( 'RunJobs' ),
'Search' => array( 'Search' ),
'Shortpages' => array( 'ShortPages' ),
'Specialpages' => array( 'SpecialPages' ),