diff --git a/src/error/PhutilErrorHandler.php b/src/error/PhutilErrorHandler.php index 63d5b492..2fc8fe7d 100644 --- a/src/error/PhutilErrorHandler.php +++ b/src/error/PhutilErrorHandler.php @@ -6,7 +6,7 @@ * * This class takes over the PHP error and exception handlers when you call * ##PhutilErrorHandler::initialize()## and forwards all debugging information - * to a listener you install with ##PhutilErrorHandler::setErrorListener()##. + * to a listener you install with ##PhutilErrorHandler::addErrorListener()##. * * To use PhutilErrorHandler, which will enhance the messages printed to the * PHP error log, just initialize it: @@ -16,7 +16,7 @@ * To additionally install a custom listener which can print error information * to some other file or console, register a listener: * - * PhutilErrorHandler::setErrorListener($some_callback); + * PhutilErrorHandler::addErrorListener($some_callback); * * For information on writing an error listener, see * @{function:phutil_error_listener_example}. Providing a listener is optional, @@ -31,7 +31,7 @@ */ final class PhutilErrorHandler extends Phobject { - private static $errorListener = null; + private static $errorListeners = array(); private static $initialized = false; private static $traps = array(); @@ -68,8 +68,15 @@ final class PhutilErrorHandler extends Phobject { * @return void * @task config */ + public static function addErrorListener($listener) { + self::$errorListeners[] = $listener; + } + + /** + * Deprecated - use `addErrorListener`. + */ public static function setErrorListener($listener) { - self::$errorListener = $listener; + self::addErrorListener($listener); } @@ -203,12 +210,17 @@ final class PhutilErrorHandler extends Phobject { if (($num === E_USER_ERROR) || ($num === E_USER_WARNING) || - ($num === E_USER_NOTICE)) { + ($num === E_USER_NOTICE) || + ($num === E_DEPRECATED)) { + + // See T15554 - we special-case E_DEPRECATED because we don't want them + // to kill the process. + $level = ($num === E_DEPRECATED) ? self::DEPRECATED : self::ERROR; $trace = debug_backtrace(); array_shift($trace); self::dispatchErrorMessage( - self::ERROR, + $level, $str, array( 'file' => $file, @@ -380,6 +392,7 @@ final class PhutilErrorHandler extends Phobject { $timestamp = date('Y-m-d H:i:s'); switch ($event) { + case self::DEPRECATED: case self::ERROR: $default_message = sprintf( '[%s] ERROR %d: %s at [%s:%d]', @@ -432,7 +445,7 @@ final class PhutilErrorHandler extends Phobject { break; } - if (self::$errorListener) { + if (self::$errorListeners) { static $handling_error; if ($handling_error) { error_log( @@ -441,7 +454,9 @@ final class PhutilErrorHandler extends Phobject { return; } $handling_error = true; - call_user_func(self::$errorListener, $event, $value, $metadata); + foreach (self::$errorListeners as $error_listener) { + call_user_func($error_listener, $event, $value, $metadata); + } $handling_error = false; } } diff --git a/src/error/phlog.php b/src/error/phlog.php index 105e0740..52dc6af9 100644 --- a/src/error/phlog.php +++ b/src/error/phlog.php @@ -41,7 +41,7 @@ function phlog($value/* , ... */) { /** * Example @{class:PhutilErrorHandler} error listener callback. When you call - * `PhutilErrorHandler::setErrorListener()`, you must pass a callback function + * `PhutilErrorHandler::addErrorListener()`, you must pass a callback function * with the same signature as this one. * * NOTE: @{class:PhutilErrorHandler} handles writing messages to the error diff --git a/src/filesystem/Filesystem.php b/src/filesystem/Filesystem.php index 9cd52268..981d4feb 100644 --- a/src/filesystem/Filesystem.php +++ b/src/filesystem/Filesystem.php @@ -1103,12 +1103,23 @@ final class Filesystem extends Phobject { } return null; } - $stdout = head($stdout); + + // These are the only file extensions that can be executed directly + // when using proc_open() with 'bypass_shell'. + $executable_extensions = ['exe', 'bat', 'cmd', 'com']; + + foreach ($stdout as $line) { + $path = trim($line); + $ext = pathinfo($path, PATHINFO_EXTENSION); + if (in_array($ext, $executable_extensions)) { + return $path; + } + } + return null; } else { list($err, $stdout) = exec_manual('which %s', $binary); + return $err === 0 ? trim($stdout) : null; } - - return $err === 0 ? trim($stdout) : null; } diff --git a/src/filesystem/PhutilErrorLog.php b/src/filesystem/PhutilErrorLog.php index d652d854..a9ec08a4 100644 --- a/src/filesystem/PhutilErrorLog.php +++ b/src/filesystem/PhutilErrorLog.php @@ -83,7 +83,7 @@ final class PhutilErrorLog } public function onError($event, $value, array $metadata) { - // If we've set "error_log" to a real file, so messages won't be output to + // If we've set "error_log" to a real file, messages won't be output to // stderr by default. Copy them to stderr. if ($this->logPath === null) { diff --git a/src/future/oauth/PhutilOAuth1Future.php b/src/future/oauth/PhutilOAuth1Future.php index 8edd6c26..f73b9db0 100644 --- a/src/future/oauth/PhutilOAuth1Future.php +++ b/src/future/oauth/PhutilOAuth1Future.php @@ -229,7 +229,10 @@ final class PhutilOAuth1Future extends FutureProxy { $consumer_secret = $this->consumerSecret->openEnvelope(); } - $key = urlencode($consumer_secret).'&'.urlencode($this->tokenSecret); + $key = urlencode($consumer_secret).'&'; + if ($this->tokenSecret !== null) { + $key .= urlencode($this->tokenSecret); + } switch ($this->signatureMethod) { case 'HMAC-SHA1': diff --git a/src/lint/linter/ArcanistComposerLinter.php b/src/lint/linter/ArcanistComposerLinter.php index a9eded3e..c46657e0 100644 --- a/src/lint/linter/ArcanistComposerLinter.php +++ b/src/lint/linter/ArcanistComposerLinter.php @@ -37,11 +37,12 @@ final class ArcanistComposerLinter extends ArcanistLinter { } private function lintComposerJson($path) { - $composer_hash = md5(Filesystem::readFile(dirname($path).'/composer.json')); + $composer_hash = self::getContentHash( + Filesystem::readFile(dirname($path).'/composer.json')); $composer_lock = phutil_json_decode( Filesystem::readFile(dirname($path).'/composer.lock')); - if ($composer_hash !== $composer_lock['hash']) { + if ($composer_hash !== $composer_lock['content-hash']) { $this->raiseLintAtPath( self::LINT_OUT_OF_DATE, pht( @@ -52,4 +53,68 @@ final class ArcanistComposerLinter extends ArcanistLinter { } } + /** + * Returns the md5 hash of the sorted content of the composer.json file. + * + * This function copied from + * https://github.com/ + * composer/composer/blob/1.5.2/src/Composer/Package/Locker.php + * and has the following license: + * + * Copyright (c) Nils Adermann, Jordi Boggiano + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * + * @param string $composer_file_contents The contents of the composer file. + * + * @return string + */ + public static function getContentHash($composer_file_contents) { + $content = json_decode($composer_file_contents, true); + + $relevant_keys = array( + 'name', + 'version', + 'require', + 'require-dev', + 'conflict', + 'replace', + 'provide', + 'minimum-stability', + 'prefer-stable', + 'repositories', + 'extra', + ); + + $relevant_content = array(); + + foreach (array_intersect($relevant_keys, array_keys($content)) as $key) { + $relevant_content[$key] = $content[$key]; + } + if (isset($content['config']['platform'])) { + $relevant_content['config']['platform'] = $content['config']['platform']; + } + + ksort($relevant_content); + + return md5(json_encode($relevant_content)); + } + } diff --git a/src/lint/linter/ArcanistExternalLinter.php b/src/lint/linter/ArcanistExternalLinter.php index a394528f..8c35e440 100644 --- a/src/lint/linter/ArcanistExternalLinter.php +++ b/src/lint/linter/ArcanistExternalLinter.php @@ -133,7 +133,19 @@ abstract class ArcanistExternalLinter extends ArcanistFutureLinter { * @task bin */ final public function getBinary() { - return coalesce($this->bin, $this->getDefaultBinary()); + $bin = coalesce($this->bin, $this->getDefaultBinary()); + if (phutil_is_windows()) { + // On Windows, we use proc_open with 'bypass_shell' option, which will + // resolve %PATH%, but not %PATHEXT% (unless the extension is .exe). + // Therefore find the right binary ourselves. + // If we can't find it, leave it unresolved, as this string will be + // used in some error messages elsewhere. + $resolved = Filesystem::resolveBinary($bin); + if ($resolved) { + return $resolved; + } + } + return $bin; } /** diff --git a/src/lint/linter/__tests__/jshint/too-many-errors.lint-test b/src/lint/linter/__tests__/jshint/too-many-errors.lint-test index e7b936dd..bc33c0dd 100644 --- a/src/lint/linter/__tests__/jshint/too-many-errors.lint-test +++ b/src/lint/linter/__tests__/jshint/too-many-errors.lint-test @@ -1,5 +1,6 @@ /* jshint maxerr: 1 */ -console.log('foobar') +console.log( +{ ~~~~~~~~~~ -disabled:2:22:E043 -warning:2:22:W033 +disabled:3:1:E043 +error:3:1:E019 diff --git a/src/lint/linter/__tests__/xml/languages-6.lint-test b/src/lint/linter/__tests__/xml/languages-6.lint-test index f652fbb8..7f14c97f 100644 --- a/src/lint/linter/__tests__/xml/languages-6.lint-test +++ b/src/lint/linter/__tests__/xml/languages-6.lint-test @@ -3,5 +3,5 @@ ~~~~~~~~~~ -error:3:16:XML76:LibXML Error +error:3:12:XML76:LibXML Error error:4:1:XML5:LibXML Error diff --git a/src/moduleutils/PhutilLibraryMapBuilder.php b/src/moduleutils/PhutilLibraryMapBuilder.php index f51fc464..df05050a 100644 --- a/src/moduleutils/PhutilLibraryMapBuilder.php +++ b/src/moduleutils/PhutilLibraryMapBuilder.php @@ -469,10 +469,12 @@ EOPHP; $this->fileSymbolMap = $symbol_map; - // We're done building the cache, so write it out immediately. Note that - // we've only retained entries for files we found, so this implicitly cleans - // out old cache entries. - $this->writeSymbolCache($symbol_map, $source_map); + if ($futures) { + // We're done building/updating the cache, so write it out immediately. + // Note that we've only retained entries for files we found, so this + // implicitly cleans out old cache entries. + $this->writeSymbolCache($symbol_map, $source_map); + } // Our map is up to date, so either show it on stdout or write it to disk. $this->librarySymbolMap = $this->buildLibraryMap($symbol_map); diff --git a/src/parser/ArcanistBundle.php b/src/parser/ArcanistBundle.php index 4617c9b6..62deca5e 100644 --- a/src/parser/ArcanistBundle.php +++ b/src/parser/ArcanistBundle.php @@ -762,7 +762,7 @@ final class ArcanistBundle extends Phobject { $old_data = $this->getBlob($old_phid, $name); } - $old_length = strlen($old_data); + $old_length = phutil_nonempty_string($old_data) ? strlen($old_data) : 0; // Here, and below, the binary will be emitted with base85 encoding. This // encoding encodes each 4 bytes of input in 5 bytes of output, so we may @@ -795,7 +795,7 @@ final class ArcanistBundle extends Phobject { $new_data = $this->getBlob($new_phid, $name); } - $new_length = strlen($new_data); + $new_length = phutil_nonempty_string($new_data) ? strlen($new_data) : 0; $this->reserveBytes($new_length * 5 / 4); if ($new_data === null) { diff --git a/src/platform/PlatformSymbols.php b/src/platform/PlatformSymbols.php index 1b02b775..2529780e 100644 --- a/src/platform/PlatformSymbols.php +++ b/src/platform/PlatformSymbols.php @@ -11,6 +11,14 @@ final class PlatformSymbols return 'Phorge'; } + public static function getPlatformClientPath() { + return 'arcanist/'; + } + + public static function getPlatformServerPath() { + return 'phorge/'; + } + public static function getProductNames() { return array( self::getPlatformClientName(), diff --git a/src/utils/PhutilCowsay.php b/src/utils/PhutilCowsay.php index ccbeccf4..aeabf145 100644 --- a/src/utils/PhutilCowsay.php +++ b/src/utils/PhutilCowsay.php @@ -41,44 +41,44 @@ final class PhutilCowsay extends Phobject { $template = $this->template; // Real ".cow" files are Perl scripts which define a variable called - // "$the_cow". We aren't going to interpret Perl, so strip all this stuff - // (and any comments in the file) away. - $template = phutil_split_lines($template, true); - $keep = array(); - $is_perl_cowfile = false; - foreach ($template as $key => $line) { - if (preg_match('/^#/', $line)) { - continue; - } - if (preg_match('/^\s*\\$the_cow/', $line)) { - $is_perl_cowfile = true; - continue; - } - if (preg_match('/^\s*EOC\s*$/', $line)) { - continue; - } - $keep[] = $line; - } - $template = implode('', $keep); + // "$the_cow". We aren't going to interpret Perl, so just get everything + // between the EOC (End Of Cow) tokens. The initial EOC might be in + // quotes, and might have a semicolon. + // We apply regexp modifiers + // * 's' to make . match newlines within the EOC ... EOC block + // * 'm' so we can use ^ to match start of line within the multiline string + $matches = null; + if ( + preg_match('/\$the_cow/', $template) && + preg_match('/EOC[\'"]?;?.*?^(.*?)^EOC/sm', $template, $matches) + ) { + $template = $matches[1]; - // Original .cow files are perl scripts which contain escaped sequences. - // We attempt to unescape here by replacing any character preceded by a - // backslash/escape with just that character. - if ($is_perl_cowfile) { + // Original .cow files are perl scripts which contain escaped sequences. + // We attempt to unescape here by replacing any character preceded by a + // backslash/escape with just that character. $template = preg_replace( '/\\\\(.)/', '$1', $template); + } else { + // Text template. Just strip away comments. + $template = preg_replace('/^#.*$/', '', $template); } - $template = preg_replace_callback( + $token_patterns = array( '/\\$([a-z]+)/', - array($this, 'replaceTemplateVariable'), - $template); - if ($template === false) { - throw new Exception( - pht( - 'Failed to replace template variables while rendering cow!')); + '/\\${([a-z]+)}/', + ); + foreach ($token_patterns as $token_pattern) { + $template = preg_replace_callback( + $token_pattern, + array($this, 'replaceTemplateVariable'), + $template); + if ($template === false) { + throw new Exception( + pht('Failed to replace template variables while rendering cow!')); + } } $lines = $this->text; diff --git a/src/utils/__tests__/cowsay/cube_perl.test b/src/utils/__tests__/cowsay/cube_perl.test index d9582d2b..a950e588 100644 --- a/src/utils/__tests__/cowsay/cube_perl.test +++ b/src/utils/__tests__/cowsay/cube_perl.test @@ -1,5 +1,5 @@ # test case for original perl-script cowfile -$the_cow = +$the_cow = < + ------------------ + \ + \ + __ + UooU\.'@@@@@@`. + \__/(@@@@@@@@@@) + (@@@@@@@@) + `YY~~~~YY' + || || diff --git a/src/utils/__tests__/cowsay/sheep.test b/src/utils/__tests__/cowsay/sheep.test new file mode 100644 index 00000000..abd54dfc --- /dev/null +++ b/src/utils/__tests__/cowsay/sheep.test @@ -0,0 +1,13 @@ + $thoughts + $thoughts + __ + U${eyes}U\.'@@@@@@`. + \__/(@@@@@@@@@@) + (@@@@@@@@) + `YY~~~~YY' + || || +~~~~~~~~~~ +{ + "text": "How are my eyes?", + "eyes": "oo" +} diff --git a/src/utils/__tests__/cowsay/small.expect b/src/utils/__tests__/cowsay/small.expect new file mode 100644 index 00000000..9d112578 --- /dev/null +++ b/src/utils/__tests__/cowsay/small.expect @@ -0,0 +1,7 @@ + __________________ +< How are my eyes? > + ------------------ + \ ,__, + \ (oo)____ + (__) )\ + ||--|| * diff --git a/src/utils/__tests__/cowsay/small.test b/src/utils/__tests__/cowsay/small.test new file mode 100644 index 00000000..bc3e08e4 --- /dev/null +++ b/src/utils/__tests__/cowsay/small.test @@ -0,0 +1,12 @@ +$eyes = ".." unless ($eyes); +$the_cow = <getConduit(); $conduit->setConduitToken($token); @@ -2244,8 +2244,8 @@ abstract class ArcanistWorkflow extends Phobject { protected function getModernUnitDictionary(array $map) { $map = $this->getModernCommonDictionary($map); - $details = idx($map, 'userData', ''); - if (strlen($details)) { + $details = idx($map, 'userData'); + if (phutil_nonempty_string($details)) { $map['details'] = (string)$details; } unset($map['userData']);