MaxSem has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/319505

Change subject: WIP: replace wfShellExec() with a class
......................................................................

WIP: replace wfShellExec() with a class

This function has gotten so unwieldy that a helper was
introduced. Instead, I'm proposing this class that makes
shelling out easier and more readable.

Example usage:
  $command = Shell::command( 'shell command' )
       ->environment( [ 'ENVIRONMENT_VARIABLE' => 'VALUE' ] )
       ->limits( [ 'time' => 300 ] )
       ->execute();

  $exitCode = $command->getExitCode();
  $output = $command->getOutput();

This is a minimal change, so lots of stuff remains
unrefactored - I'd rather limit the scope of this commit.
A future improvement could be an ability to get stderr
separately from stdout.

Problem points that I'd like to discuss:
* I went with a purely OOP approach for handling the 'proc_open()
  is disabled' error - throw an exception and forget. While
  stubbed wfShellExec() retains the old behavior, once callers
  are updated they will either need to catch the exception or they
  will break for some poor souls - another option would be to introduce
  a noThrow() option.
* proc_open() failures currently just return a status code - do something
  about that, too?

Change-Id: I8ac9858b80d7908cf7e7981d7e19d0fc9c2265c0
---
M autoload.php
M includes/GlobalFunctions.php
A includes/exception/ShellDisabledError.php
A includes/shell/Shell.php
4 files changed, 444 insertions(+), 290 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/05/319505/1

diff --git a/autoload.php b/autoload.php
index b96250d..27952b3 100644
--- a/autoload.php
+++ b/autoload.php
@@ -904,6 +904,8 @@
        'MediaWiki\\Session\\SessionProviderInterface' => __DIR__ . 
'/includes/session/SessionProviderInterface.php',
        'MediaWiki\\Session\\Token' => __DIR__ . '/includes/session/Token.php',
        'MediaWiki\\Session\\UserInfo' => __DIR__ . 
'/includes/session/UserInfo.php',
+       'MediaWiki\\Shell' => __DIR__ . '/includes/shell/Shell.php',
+       'MediaWiki\\ShellDisabledError' => __DIR__ . 
'/includes/exception/ShellDisabledError.php',
        'MediaWiki\\Site\\MediaWikiPageNameNormalizer' => __DIR__ . 
'/includes/site/MediaWikiPageNameNormalizer.php',
        'MediaWiki\\Tidy\\BalanceActiveFormattingElements' => __DIR__ . 
'/includes/tidy/Balancer.php',
        'MediaWiki\\Tidy\\BalanceElement' => __DIR__ . 
'/includes/tidy/Balancer.php',
diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php
index bae9c77..e6d3f24 100644
--- a/includes/GlobalFunctions.php
+++ b/includes/GlobalFunctions.php
@@ -27,6 +27,7 @@
 use Liuggio\StatsdClient\Sender\SocketSender;
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\Session\SessionManager;
+use MediaWiki\Shell;
 use Wikimedia\ScopedCallback;
 
 // Hide compatibility functions from Doxygen
@@ -2205,66 +2206,11 @@
  *
  * @param string ... strings to escape and glue together, or a single array of 
strings parameter
  * @return string
+ * @deprecated since 1.29 use MediaWiki\Shell::escape()
  */
 function wfEscapeShellArg( /*...*/ ) {
-       wfInitShellLocale();
-
        $args = func_get_args();
-       if ( count( $args ) === 1 && is_array( reset( $args ) ) ) {
-               // If only one argument has been passed, and that argument is 
an array,
-               // treat it as a list of arguments
-               $args = reset( $args );
-       }
-
-       $first = true;
-       $retVal = '';
-       foreach ( $args as $arg ) {
-               if ( !$first ) {
-                       $retVal .= ' ';
-               } else {
-                       $first = false;
-               }
-
-               if ( wfIsWindows() ) {
-                       // Escaping for an MSVC-style command line parser and 
CMD.EXE
-                       // @codingStandardsIgnoreStart For long URLs
-                       // Refs:
-                       //  * 
http://web.archive.org/web/20020708081031/http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html
-                       //  * 
http://technet.microsoft.com/en-us/library/cc723564.aspx
-                       //  * T15518
-                       //  * CR r63214
-                       // Double the backslashes before any double quotes. 
Escape the double quotes.
-                       // @codingStandardsIgnoreEnd
-                       $tokens = preg_split( '/(\\\\*")/', $arg, -1, 
PREG_SPLIT_DELIM_CAPTURE );
-                       $arg = '';
-                       $iteration = 0;
-                       foreach ( $tokens as $token ) {
-                               if ( $iteration % 2 == 1 ) {
-                                       // Delimiter, a double quote preceded 
by zero or more slashes
-                                       $arg .= str_replace( '\\', '\\\\', 
substr( $token, 0, -1 ) ) . '\\"';
-                               } elseif ( $iteration % 4 == 2 ) {
-                                       // ^ in $token will be outside quotes, 
need to be escaped
-                                       $arg .= str_replace( '^', '^^', $token 
);
-                               } else { // $iteration % 4 == 0
-                                       // ^ in $token will appear inside 
double quotes, so leave as is
-                                       $arg .= $token;
-                               }
-                               $iteration++;
-                       }
-                       // Double the backslashes before the end of the string, 
because
-                       // we will soon add a quote
-                       $m = [];
-                       if ( preg_match( '/^(.*?)(\\\\+)$/', $arg, $m ) ) {
-                               $arg = $m[1] . str_replace( '\\', '\\\\', $m[2] 
);
-                       }
-
-                       // Add surrounding quotes
-                       $retVal .= '"' . $arg . '"';
-               } else {
-                       $retVal .= escapeshellarg( $arg );
-               }
-       }
-       return $retVal;
+       return call_user_func( 'MediaWiki\\Shell::escape', $args );
 }
 
 /**
@@ -2272,18 +2218,10 @@
  *
  * @return bool|string False or 'disabled'
  * @since 1.22
+ * @deprecated since 1.29 use MediaWiki\Shell::isDisabled()
  */
 function wfShellExecDisabled() {
-       static $disabled = null;
-       if ( is_null( $disabled ) ) {
-               if ( !function_exists( 'proc_open' ) ) {
-                       wfDebug( "proc_open() is disabled\n" );
-                       $disabled = 'disabled';
-               } else {
-                       $disabled = false;
-               }
-       }
-       return $disabled;
+       return Shell::isDisabled() ? 'disabled' : false;
 }
 
 /**
@@ -2307,15 +2245,12 @@
  *     method. Set this to a string for an alternative method to profile from
  *
  * @return string Collected stdout as a string
+ * @deprecated since 1.29 use class MediaWiki\Shell
  */
 function wfShellExec( $cmd, &$retval = null, $environ = [],
        $limits = [], $options = []
 ) {
-       global $IP, $wgMaxShellMemory, $wgMaxShellFileSize, $wgMaxShellTime,
-               $wgMaxShellWallClockTime, $wgShellCgroup;
-
-       $disabled = wfShellExecDisabled();
-       if ( $disabled ) {
+       if ( Shell::isDisabled() ) {
                $retval = 1;
                return 'Unable to run external programs, proc_open() is 
disabled.';
        }
@@ -2323,207 +2258,16 @@
        $includeStderr = isset( $options['duplicateStderr'] ) && 
$options['duplicateStderr'];
        $profileMethod = isset( $options['profileMethod'] ) ? 
$options['profileMethod'] : wfGetCaller();
 
-       wfInitShellLocale();
+       $command = Shell::command( $cmd )
+               ->environment( $environ )
+               ->limits( $limits )
+               ->includeStderr( $includeStderr )
+               ->profileMethod( $profileMethod )
+               ->execute();
 
-       $envcmd = '';
-       foreach ( $environ as $k => $v ) {
-               if ( wfIsWindows() ) {
-                       /* Surrounding a set in quotes (method used by 
wfEscapeShellArg) makes the quotes themselves
-                        * appear in the environment variable, so we must use 
carat escaping as documented in
-                        * 
http://technet.microsoft.com/en-us/library/cc723564.aspx
-                        * Note however that the quote isn't listed there, but 
is needed, and the parentheses
-                        * are listed there but doesn't appear to need it.
-                        */
-                       $envcmd .= "set $k=" . preg_replace( '/([&|()<>^"])/', 
'^\\1', $v ) . '&& ';
-               } else {
-                       /* Assume this is a POSIX shell, thus required to 
accept variable assignments before the command
-                        * 
http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_09_01
-                        */
-                       $envcmd .= "$k=" . escapeshellarg( $v ) . ' ';
-               }
-       }
-       if ( is_array( $cmd ) ) {
-               $cmd = wfEscapeShellArg( $cmd );
-       }
+       $retval = $command->getExitCode();
 
-       $cmd = $envcmd . $cmd;
-
-       $useLogPipe = false;
-       if ( is_executable( '/bin/bash' ) ) {
-               $time = intval( isset( $limits['time'] ) ? $limits['time'] : 
$wgMaxShellTime );
-               if ( isset( $limits['walltime'] ) ) {
-                       $wallTime = intval( $limits['walltime'] );
-               } elseif ( isset( $limits['time'] ) ) {
-                       $wallTime = $time;
-               } else {
-                       $wallTime = intval( $wgMaxShellWallClockTime );
-               }
-               $mem = intval( isset( $limits['memory'] ) ? $limits['memory'] : 
$wgMaxShellMemory );
-               $filesize = intval( isset( $limits['filesize'] ) ? 
$limits['filesize'] : $wgMaxShellFileSize );
-
-               if ( $time > 0 || $mem > 0 || $filesize > 0 || $wallTime > 0 ) {
-                       $cmd = '/bin/bash ' . escapeshellarg( 
"$IP/includes/limit.sh" ) . ' ' .
-                               escapeshellarg( $cmd ) . ' ' .
-                               escapeshellarg(
-                                       "MW_INCLUDE_STDERR=" . ( $includeStderr 
? '1' : '' ) . ';' .
-                                       "MW_CPU_LIMIT=$time; " .
-                                       'MW_CGROUP=' . escapeshellarg( 
$wgShellCgroup ) . '; ' .
-                                       "MW_MEM_LIMIT=$mem; " .
-                                       "MW_FILE_SIZE_LIMIT=$filesize; " .
-                                       "MW_WALL_CLOCK_LIMIT=$wallTime; " .
-                                       "MW_USE_LOG_PIPE=yes"
-                               );
-                       $useLogPipe = true;
-               } elseif ( $includeStderr ) {
-                       $cmd .= ' 2>&1';
-               }
-       } elseif ( $includeStderr ) {
-               $cmd .= ' 2>&1';
-       }
-       wfDebug( "wfShellExec: $cmd\n" );
-
-       // Don't try to execute commands that exceed Linux's MAX_ARG_STRLEN.
-       // Other platforms may be more accomodating, but we don't want to be
-       // accomodating, because very long commands probably include user
-       // input. See T129506.
-       if ( strlen( $cmd ) > SHELL_MAX_ARG_STRLEN ) {
-               throw new Exception( __METHOD__ .
-                       '(): total length of $cmd must not exceed 
SHELL_MAX_ARG_STRLEN' );
-       }
-
-       $desc = [
-               0 => [ 'file', 'php://stdin', 'r' ],
-               1 => [ 'pipe', 'w' ],
-               2 => [ 'file', 'php://stderr', 'w' ] ];
-       if ( $useLogPipe ) {
-               $desc[3] = [ 'pipe', 'w' ];
-       }
-       $pipes = null;
-       $scoped = Profiler::instance()->scopedProfileIn( __FUNCTION__ . '-' . 
$profileMethod );
-       $proc = proc_open( $cmd, $desc, $pipes );
-       if ( !$proc ) {
-               wfDebugLog( 'exec', "proc_open() failed: $cmd" );
-               $retval = -1;
-               return '';
-       }
-       $outBuffer = $logBuffer = '';
-       $emptyArray = [];
-       $status = false;
-       $logMsg = false;
-
-       /* According to the documentation, it is possible for stream_select()
-        * to fail due to EINTR. I haven't managed to induce this in testing
-        * despite sending various signals. If it did happen, the error
-        * message would take the form:
-        *
-        * stream_select(): unable to select [4]: Interrupted system call 
(max_fd=5)
-        *
-        * where [4] is the value of the macro EINTR and "Interrupted system
-        * call" is string which according to the Linux manual is "possibly"
-        * localised according to LC_MESSAGES.
-        */
-       $eintr = defined( 'SOCKET_EINTR' ) ? SOCKET_EINTR : 4;
-       $eintrMessage = "stream_select(): unable to select [$eintr]";
-
-       $running = true;
-       $timeout = null;
-       $numReadyPipes = 0;
-
-       while ( $running === true || $numReadyPipes !== 0 ) {
-               if ( $running ) {
-                       $status = proc_get_status( $proc );
-                       // If the process has terminated, switch to nonblocking 
selects
-                       // for getting any data still waiting to be read.
-                       if ( !$status['running'] ) {
-                               $running = false;
-                               $timeout = 0;
-                       }
-               }
-
-               $readyPipes = $pipes;
-
-               // Clear last error
-               // @codingStandardsIgnoreStart 
Generic.PHP.NoSilencedErrors.Discouraged
-               @trigger_error( '' );
-               $numReadyPipes = @stream_select( $readyPipes, $emptyArray, 
$emptyArray, $timeout );
-               if ( $numReadyPipes === false ) {
-                       // @codingStandardsIgnoreEnd
-                       $error = error_get_last();
-                       if ( strncmp( $error['message'], $eintrMessage, strlen( 
$eintrMessage ) ) == 0 ) {
-                               continue;
-                       } else {
-                               trigger_error( $error['message'], 
E_USER_WARNING );
-                               $logMsg = $error['message'];
-                               break;
-                       }
-               }
-               foreach ( $readyPipes as $fd => $pipe ) {
-                       $block = fread( $pipe, 65536 );
-                       if ( $block === '' ) {
-                               // End of file
-                               fclose( $pipes[$fd] );
-                               unset( $pipes[$fd] );
-                               if ( !$pipes ) {
-                                       break 2;
-                               }
-                       } elseif ( $block === false ) {
-                               // Read error
-                               $logMsg = "Error reading from pipe";
-                               break 2;
-                       } elseif ( $fd == 1 ) {
-                               // From stdout
-                               $outBuffer .= $block;
-                       } elseif ( $fd == 3 ) {
-                               // From log FD
-                               $logBuffer .= $block;
-                               if ( strpos( $block, "\n" ) !== false ) {
-                                       $lines = explode( "\n", $logBuffer );
-                                       $logBuffer = array_pop( $lines );
-                                       foreach ( $lines as $line ) {
-                                               wfDebugLog( 'exec', $line );
-                                       }
-                               }
-                       }
-               }
-       }
-
-       foreach ( $pipes as $pipe ) {
-               fclose( $pipe );
-       }
-
-       // Use the status previously collected if possible, since 
proc_get_status()
-       // just calls waitpid() which will not return anything useful the 
second time.
-       if ( $running ) {
-               $status = proc_get_status( $proc );
-       }
-
-       if ( $logMsg !== false ) {
-               // Read/select error
-               $retval = -1;
-               proc_close( $proc );
-       } elseif ( $status['signaled'] ) {
-               $logMsg = "Exited with signal {$status['termsig']}";
-               $retval = 128 + $status['termsig'];
-               proc_close( $proc );
-       } else {
-               if ( $status['running'] ) {
-                       $retval = proc_close( $proc );
-               } else {
-                       $retval = $status['exitcode'];
-                       proc_close( $proc );
-               }
-               if ( $retval == 127 ) {
-                       $logMsg = "Possibly missing executable file";
-               } elseif ( $retval >= 129 && $retval <= 192 ) {
-                       $logMsg = "Probably exited with signal " . ( $retval - 
128 );
-               }
-       }
-
-       if ( $logMsg !== false ) {
-               wfDebugLog( 'exec', "$logMsg: $cmd" );
-       }
-
-       return $outBuffer;
+       return $command->getOutput();
 }
 
 /**
@@ -2541,25 +2285,11 @@
  * @param array $limits Optional array with limits(filesize, memory, time, 
walltime)
  *   this overwrites the global wgMaxShell* limits.
  * @return string Collected stdout and stderr as a string
+ * @deprecated since 1.29 use class MediaWiki\Shell
  */
 function wfShellExecWithStderr( $cmd, &$retval = null, $environ = [], $limits 
= [] ) {
        return wfShellExec( $cmd, $retval, $environ, $limits,
                [ 'duplicateStderr' => true, 'profileMethod' => wfGetCaller() ] 
);
-}
-
-/**
- * Workaround for http://bugs.php.net/bug.php?id=45132
- * escapeshellarg() destroys non-ASCII characters if LANG is not a UTF-8 locale
- */
-function wfInitShellLocale() {
-       static $done = false;
-       if ( $done ) {
-               return;
-       }
-       $done = true;
-       global $wgShellLocale;
-       putenv( "LC_CTYPE=$wgShellLocale" );
-       setlocale( LC_CTYPE, $wgShellLocale );
 }
 
 /**
@@ -2585,7 +2315,7 @@
        }
        $cmd[] = $script;
        // Escape each parameter for shell
-       return wfEscapeShellArg( array_merge( $cmd, $parameters ) );
+       return Shell::escape( array_merge( $cmd, $parameters ) );
 }
 
 /**
@@ -2630,7 +2360,7 @@
        fclose( $yourtextFile );
 
        # Check for a conflict
-       $cmd = wfEscapeShellArg( $wgDiff3, '-a', '--overlap-only', $mytextName,
+       $cmd = Shell::escape( $wgDiff3, '-a', '--overlap-only', $mytextName,
                $oldtextName, $yourtextName );
        $handle = popen( $cmd, 'r' );
 
@@ -2642,7 +2372,7 @@
        pclose( $handle );
 
        # Merge differences
-       $cmd = wfEscapeShellArg( $wgDiff3, '-a', '-e', '--merge', $mytextName,
+       $cmd = Shell::escape( $wgDiff3, '-a', '-e', '--merge', $mytextName,
                $oldtextName, $yourtextName );
        $handle = popen( $cmd, 'r' );
        $result = '';
@@ -2706,7 +2436,7 @@
        fclose( $newtextFile );
 
        // Get the diff of the two files
-       $cmd = "$wgDiff " . $params . ' ' . wfEscapeShellArg( $oldtextName, 
$newtextName );
+       $cmd = "$wgDiff " . $params . ' ' . Shell::escape( $oldtextName, 
$newtextName );
 
        $h = popen( $cmd, 'r' );
        if ( !$h ) {
diff --git a/includes/exception/ShellDisabledError.php 
b/includes/exception/ShellDisabledError.php
new file mode 100644
index 0000000..6ad61c4
--- /dev/null
+++ b/includes/exception/ShellDisabledError.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace MediaWiki;
+
+use Exception;
+
+class ShellDisabledError extends Exception {
+       public function __construct() {
+               parent::__construct( 'Unable to run external programs, 
proc_open() is disabled' );
+       }
+}
diff --git a/includes/shell/Shell.php b/includes/shell/Shell.php
new file mode 100644
index 0000000..082f1b5
--- /dev/null
+++ b/includes/shell/Shell.php
@@ -0,0 +1,411 @@
+<?php
+/**
+ * Class used for executing shell commands
+ *
+ * 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
+ */
+
+namespace MediaWiki;
+
+use Exception;
+use Profiler;
+
+/**
+ * Executes shell commands.
+ *
+ * Use call chaining with this class for expressiveness:
+ *  Shell::command( 'shell command' )
+ *       ->environment( [ 'ENVIRONMENT_VARIABLE' => 'VALUE' ] )
+ *       ->limits( [ 'time' => 300 ] )
+ *       ->execute()
+ */
+class Shell {
+       /** @var string|string[] */
+       private $command;
+
+       /** @var array */
+       private $limits = [];
+
+       /** @var string[] */
+       private $env = [];
+
+       /** @var string */
+       private $method;
+
+       /** @var bool */
+       private $useStderr = false;
+
+       private $output;
+
+       private $exitCode;
+
+       private function __construct( $command ) {
+               if ( is_array( $command ) ) {
+                       $command = self::escape( $command );
+               }
+
+               $this->command = $command;
+       }
+
+       public static function command( $command ) {
+               return new self( $command );
+       }
+
+       /**
+        * Check if this class is effectively disabled via php.ini config
+        *
+        * @return bool
+        */
+       public static function isDisabled() {
+               static $disabled = null;
+
+               if ( is_null( $disabled ) ) {
+                       if ( !function_exists( 'proc_open' ) ) {
+                               wfDebug( "proc_open() is disabled\n" );
+                               $disabled = true;
+                       } else {
+                               $disabled = false;
+                       }
+               }
+
+               return $disabled;
+       }
+
+       public function limits( array $limits ) {
+               $this->limits = $limits;
+
+               return $this;
+       }
+
+       public function environment( array $env ) {
+               $this->env = $env;
+
+               return $this;
+       }
+
+       public function profileMethod( $method ) {
+               $this->method = $method;
+
+               return $this;
+       }
+
+       public function includeStderr( $yesno ) {
+               $this->useStderr = $yesno;
+
+               return $this;
+       }
+
+       public function execute() {
+               global $IP, $wgMaxShellMemory, $wgMaxShellFileSize, 
$wgMaxShellTime,
+                          $wgMaxShellWallClockTime, $wgShellCgroup;
+
+               if ( self::isDisabled() ) {
+                       throw new ShellDisabledError();
+               }
+
+               $profileMethod = $this->method ?: wfGetCaller();
+
+               self::initShellLocale();
+
+               $envcmd = '';
+               foreach ( $this->env as $k => $v ) {
+                       if ( wfIsWindows() ) {
+                               /* Surrounding a set in quotes (method used by 
wfEscapeShellArg) makes the quotes themselves
+                                * appear in the environment variable, so we 
must use carat escaping as documented in
+                                * 
http://technet.microsoft.com/en-us/library/cc723564.aspx
+                                * Note however that the quote isn't listed 
there, but is needed, and the parentheses
+                                * are listed there but doesn't appear to need 
it.
+                                */
+                               $envcmd .= "set $k=" . preg_replace( 
'/([&|()<>^"])/', '^\\1', $v ) . '&& ';
+                       } else {
+                               /* Assume this is a POSIX shell, thus required 
to accept variable assignments before the command
+                                * 
http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_09_01
+                                */
+                               $envcmd .= "$k=" . escapeshellarg( $v ) . ' ';
+                       }
+               }
+
+               $cmd = $envcmd . $this->command;
+
+               $useLogPipe = false;
+               if ( is_executable( '/bin/bash' ) ) {
+                       $time = intval( isset( $this->limits['time'] ) ? 
$this->limits['time'] : $wgMaxShellTime );
+                       if ( isset( $this->limits['walltime'] ) ) {
+                               $wallTime = intval( $this->limits['walltime'] );
+                       } elseif ( isset( $this->limits['time'] ) ) {
+                               $wallTime = $time;
+                       } else {
+                               $wallTime = intval( $wgMaxShellWallClockTime );
+                       }
+                       $mem = intval( isset( $this->limits['memory'] ) ? 
$this->limits['memory'] : $wgMaxShellMemory );
+                       $filesize = intval( isset( $this->limits['filesize'] ) 
? $this->limits['filesize'] : $wgMaxShellFileSize );
+
+                       if ( $time > 0 || $mem > 0 || $filesize > 0 || 
$wallTime > 0 ) {
+                               $cmd = '/bin/bash ' . escapeshellarg( 
"$IP/includes/limit.sh" ) . ' ' .
+                                          escapeshellarg( $cmd ) . ' ' .
+                                          escapeshellarg(
+                                                  "MW_INCLUDE_STDERR=" . ( 
$this->useStderr ? '1' : '' ) . ';' .
+                                                  "MW_CPU_LIMIT=$time; " .
+                                                  'MW_CGROUP=' . 
escapeshellarg( $wgShellCgroup ) . '; ' .
+                                                  "MW_MEM_LIMIT=$mem; " .
+                                                  
"MW_FILE_SIZE_LIMIT=$filesize; " .
+                                                  
"MW_WALL_CLOCK_LIMIT=$wallTime; " .
+                                                  "MW_USE_LOG_PIPE=yes"
+                                          );
+                               $useLogPipe = true;
+                       } elseif ( $this->useStderr ) {
+                               $cmd .= ' 2>&1';
+                       }
+               } elseif ( $this->useStderr ) {
+                       $cmd .= ' 2>&1';
+               }
+               wfDebug( __METHOD__ . ": $cmd\n" );
+
+               // Don't try to execute commands that exceed Linux's 
MAX_ARG_STRLEN.
+               // Other platforms may be more accomodating, but we don't want 
to be
+               // accomodating, because very long commands probably include 
user
+               // input. See T129506.
+               if ( strlen( $cmd ) > SHELL_MAX_ARG_STRLEN ) {
+                       throw new Exception( __METHOD__ .
+                                                                '(): total 
length of $cmd must not exceed SHELL_MAX_ARG_STRLEN' );
+               }
+
+               $desc = [
+                       0 => [ 'file', 'php://stdin', 'r' ],
+                       1 => [ 'pipe', 'w' ],
+                       2 => [ 'file', 'php://stderr', 'w' ] ];
+               if ( $useLogPipe ) {
+                       $desc[3] = [ 'pipe', 'w' ];
+               }
+               $pipes = null;
+               $scoped = Profiler::instance()->scopedProfileIn( __FUNCTION__ . 
'-' . $profileMethod );
+               $proc = proc_open( $cmd, $desc, $pipes );
+               if ( !$proc ) {
+                       // @todo: more structured error reporting?
+                       wfDebugLog( 'exec', "proc_open() failed: $cmd" );
+                       $this->exitCode = -1;
+                       $this->output = '';
+                       return $this;
+               }
+               $outBuffer = $logBuffer = '';
+               $emptyArray = [];
+               $status = false;
+               $logMsg = false;
+
+               /* According to the documentation, it is possible for 
stream_select()
+                * to fail due to EINTR. I haven't managed to induce this in 
testing
+                * despite sending various signals. If it did happen, the error
+                * message would take the form:
+                *
+                * stream_select(): unable to select [4]: Interrupted system 
call (max_fd=5)
+                *
+                * where [4] is the value of the macro EINTR and "Interrupted 
system
+                * call" is string which according to the Linux manual is 
"possibly"
+                * localised according to LC_MESSAGES.
+                */
+               $eintr = defined( 'SOCKET_EINTR' ) ? SOCKET_EINTR : 4;
+               $eintrMessage = "stream_select(): unable to select [$eintr]";
+
+               $running = true;
+               $timeout = null;
+               $numReadyPipes = 0;
+
+               while ( $running === true || $numReadyPipes !== 0 ) {
+                       if ( $running ) {
+                               $status = proc_get_status( $proc );
+                               // If the process has terminated, switch to 
nonblocking selects
+                               // for getting any data still waiting to be 
read.
+                               if ( !$status['running'] ) {
+                                       $running = false;
+                                       $timeout = 0;
+                               }
+                       }
+
+                       $readyPipes = $pipes;
+
+                       // Clear last error
+                       // @codingStandardsIgnoreStart 
Generic.PHP.NoSilencedErrors.Discouraged
+                       @trigger_error( '' );
+                       $numReadyPipes = @stream_select( $readyPipes, 
$emptyArray, $emptyArray, $timeout );
+                       if ( $numReadyPipes === false ) {
+                               // @codingStandardsIgnoreEnd
+                               $error = error_get_last();
+                               if ( strncmp( $error['message'], $eintrMessage, 
strlen( $eintrMessage ) ) == 0 ) {
+                                       continue;
+                               } else {
+                                       trigger_error( $error['message'], 
E_USER_WARNING );
+                                       $logMsg = $error['message'];
+                                       break;
+                               }
+                       }
+                       foreach ( $readyPipes as $fd => $pipe ) {
+                               $block = fread( $pipe, 65536 );
+                               if ( $block === '' ) {
+                                       // End of file
+                                       fclose( $pipes[$fd] );
+                                       unset( $pipes[$fd] );
+                                       if ( !$pipes ) {
+                                               break 2;
+                                       }
+                               } elseif ( $block === false ) {
+                                       // Read error
+                                       $logMsg = "Error reading from pipe";
+                                       break 2;
+                               } elseif ( $fd == 1 ) {
+                                       // From stdout
+                                       $outBuffer .= $block;
+                               } elseif ( $fd == 3 ) {
+                                       // From log FD
+                                       $logBuffer .= $block;
+                                       if ( strpos( $block, "\n" ) !== false ) 
{
+                                               $lines = explode( "\n", 
$logBuffer );
+                                               $logBuffer = array_pop( $lines 
);
+                                               foreach ( $lines as $line ) {
+                                                       wfDebugLog( 'exec', 
$line );
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               foreach ( $pipes as $pipe ) {
+                       fclose( $pipe );
+               }
+
+               // Use the status previously collected if possible, since 
proc_get_status()
+               // just calls waitpid() which will not return anything useful 
the second time.
+               if ( $running ) {
+                       $status = proc_get_status( $proc );
+               }
+
+               if ( $logMsg !== false ) {
+                       // Read/select error
+                       $retval = -1;
+                       proc_close( $proc );
+               } elseif ( $status['signaled'] ) {
+                       $logMsg = "Exited with signal {$status['termsig']}";
+                       $retval = 128 + $status['termsig'];
+                       proc_close( $proc );
+               } else {
+                       if ( $status['running'] ) {
+                               $retval = proc_close( $proc );
+                       } else {
+                               $retval = $status['exitcode'];
+                               proc_close( $proc );
+                       }
+                       if ( $retval == 127 ) {
+                               $logMsg = "Possibly missing executable file";
+                       } elseif ( $retval >= 129 && $retval <= 192 ) {
+                               $logMsg = "Probably exited with signal " . ( 
$retval - 128 );
+                       }
+               }
+
+               if ( $logMsg !== false ) {
+                       wfDebugLog( 'exec', "$logMsg: $cmd" );
+               }
+
+               $this->output = $outBuffer;
+               $this->exitCode = $retval;
+
+               return $this;
+       }
+
+       public function getExitCode() {
+               return $this->exitCode;
+       }
+
+       public function getOutput() {
+               return $this->output;
+       }
+
+       public static function escape( /* ... */ ) {
+               self::initShellLocale();
+
+               $args = func_get_args();
+               if ( count( $args ) === 1 && is_array( reset( $args ) ) ) {
+                       // If only one argument has been passed, and that 
argument is an array,
+                       // treat it as a list of arguments
+                       $args = reset( $args );
+               }
+
+               $first = true;
+               $retVal = '';
+               foreach ( $args as $arg ) {
+                       if ( !$first ) {
+                               $retVal .= ' ';
+                       } else {
+                               $first = false;
+                       }
+
+                       if ( wfIsWindows() ) {
+                               // Escaping for an MSVC-style command line 
parser and CMD.EXE
+                               // @codingStandardsIgnoreStart For long URLs
+                               // Refs:
+                               //  * 
http://web.archive.org/web/20020708081031/http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html
+                               //  * 
http://technet.microsoft.com/en-us/library/cc723564.aspx
+                               //  * T15518
+                               //  * CR r63214
+                               // Double the backslashes before any double 
quotes. Escape the double quotes.
+                               // @codingStandardsIgnoreEnd
+                               $tokens = preg_split( '/(\\\\*")/', $arg, -1, 
PREG_SPLIT_DELIM_CAPTURE );
+                               $arg = '';
+                               $iteration = 0;
+                               foreach ( $tokens as $token ) {
+                                       if ( $iteration % 2 == 1 ) {
+                                               // Delimiter, a double quote 
preceded by zero or more slashes
+                                               $arg .= str_replace( '\\', 
'\\\\', substr( $token, 0, -1 ) ) . '\\"';
+                                       } elseif ( $iteration % 4 == 2 ) {
+                                               // ^ in $token will be outside 
quotes, need to be escaped
+                                               $arg .= str_replace( '^', '^^', 
$token );
+                                       } else { // $iteration % 4 == 0
+                                               // ^ in $token will appear 
inside double quotes, so leave as is
+                                               $arg .= $token;
+                                       }
+                                       $iteration++;
+                               }
+                               // Double the backslashes before the end of the 
string, because
+                               // we will soon add a quote
+                               $m = [];
+                               if ( preg_match( '/^(.*?)(\\\\+)$/', $arg, $m ) 
) {
+                                       $arg = $m[1] . str_replace( '\\', 
'\\\\', $m[2] );
+                               }
+
+                               // Add surrounding quotes
+                               $retVal .= '"' . $arg . '"';
+                       } else {
+                               $retVal .= escapeshellarg( $arg );
+                       }
+               }
+               return $retVal;
+       }
+
+       /**
+        * Workaround for http://bugs.php.net/bug.php?id=45132
+        * escapeshellarg() destroys non-ASCII characters if LANG is not a 
UTF-8 locale
+        */
+       private static function initShellLocale() {
+               static $done = false;
+               if ( $done ) {
+                       return;
+               }
+               $done = true;
+               global $wgShellLocale;
+               putenv( "LC_CTYPE=$wgShellLocale" );
+               setlocale( LC_CTYPE, $wgShellLocale );
+       }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/319505
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I8ac9858b80d7908cf7e7981d7e19d0fc9c2265c0
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: MaxSem <maxsem.w...@gmail.com>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to