Avoid header notice log spam from RunJobs API
authorAaron Schulz <aschulz@wikimedia.org>
Wed, 12 Mar 2014 19:19:27 +0000 (12:19 -0700)
committerOri.livneh <ori@wikimedia.org>
Tue, 18 Mar 2014 22:30:50 +0000 (22:30 +0000)
* Moved ApiRunJobs to a special page instead of going through
  ApiMain and having to fight the logic there. As a separate
  internal API, this does not show up on the API help page and
  is no longer effected by $wgEnableAPI.

bug: 62233
Change-Id: I1db6f526d02e130a66ee03289858a734d89e6c00

includes/AutoLoader.php
includes/Wiki.php
includes/api/ApiMain.php
includes/api/ApiRunJobs.php [deleted file]
includes/specialpage/SpecialPageFactory.php
includes/specials/SpecialRunJobs.php [new file with mode: 0644]
languages/messages/MessagesEn.php

index c627da1..d5c28ba 100644 (file)
@@ -361,7 +361,6 @@ $wgAutoloadLocalClasses = array(
        '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',
@@ -1018,6 +1017,7 @@ $wgAutoloadLocalClasses = array(
        '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',
index 69cfe9d..4bf8fd3 100644 (file)
@@ -624,10 +624,12 @@ class MediaWiki {
         * 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__ );
@@ -642,15 +644,9 @@ class MediaWiki {
                        $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 );
@@ -665,11 +661,11 @@ class MediaWiki {
                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" );
index 8f270dc..e1c0874 100644 (file)
@@ -68,7 +68,6 @@ class ApiMain extends ApiBase {
                'purge' => 'ApiPurge',
                'setnotificationtimestamp' => 'ApiSetNotificationTimestamp',
                'rollback' => 'ApiRollback',
-               'runjobs' => 'ApiRunJobs',
                'delete' => 'ApiDelete',
                'undelete' => 'ApiUndelete',
                'protect' => 'ApiProtect',
diff --git a/includes/api/ApiRunJobs.php b/includes/api/ApiRunJobs.php
deleted file mode 100644 (file)
index 05327c8..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-<?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',
-               );
-       }
-}
index dea65f3..c6735e6 100644 (file)
@@ -165,6 +165,7 @@ class SpecialPageFactory {
                'PermanentLink'             => 'SpecialPermanentLink',
                'Redirect'                  => 'SpecialRedirect',
                'Revisiondelete'            => 'SpecialRevisionDelete',
+               'RunJobs'                   => 'SpecialRunJobs',
                'Specialpages'              => 'SpecialSpecialpages',
                'Userlogout'                => 'SpecialUserlogout',
        );
diff --git a/includes/specials/SpecialRunJobs.php b/includes/specials/SpecialRunJobs.php
new file mode 100644 (file)
index 0000000..8a4026d
--- /dev/null
@@ -0,0 +1,157 @@
+<?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 );
+               }
+       }
+}
index f1725a3..113bb24 100644 (file)
@@ -460,6 +460,7 @@ $specialPageAliases = array(
        'Redirect'                  => array( 'Redirect' ),
        'ResetTokens'               => array( 'ResetTokens' ),
        'Revisiondelete'            => array( 'RevisionDelete' ),
+       'RunJobs'                   => array( 'RunJobs' ),
        'Search'                    => array( 'Search' ),
        'Shortpages'                => array( 'ShortPages' ),
        'Specialpages'              => array( 'SpecialPages' ),