diff --git a/externals/porter-stemmer/LICENSE b/externals/porter-stemmer/LICENSE new file mode 100644 index 0000000000..d4afc6cfa7 --- /dev/null +++ b/externals/porter-stemmer/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2005-2016 Richard Heyes (http://www.phpguru.org/) + +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. diff --git a/externals/porter-stemmer/README.md b/externals/porter-stemmer/README.md new file mode 100644 index 0000000000..06bc417f9f --- /dev/null +++ b/externals/porter-stemmer/README.md @@ -0,0 +1,42 @@ +# Porter Stemmer by Richard Heyes + +# Installation (with composer) + +```json +{ + "require": { + "camspiers/porter-stemmer": "1.0.0" + } +} +``` + + $ composer install + +# Usage + +```php +$stem = Porter::Stem($word); +``` + +# License + +The MIT License (MIT) + +Copyright (c) 2005-2016 Richard Heyes (http://www.phpguru.org/) + +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. diff --git a/externals/porter-stemmer/src/Porter.php b/externals/porter-stemmer/src/Porter.php new file mode 100644 index 0000000000..b8715dade1 --- /dev/null +++ b/externals/porter-stemmer/src/Porter.php @@ -0,0 +1,426 @@ + + * + * Originally available under the GPL 2 or greater. Relicensed with permission + * of original authors under the MIT License in 2016. + * + * All rights reserved. + * + * @package PorterStemmer + * @author Richard Heyes + * @author Jon Abernathy + * @copyright 2005-2016 Richard Heyes (http://www.phpguru.org/) + * @license http://www.opensource.org/licenses/mit-license.html MIT License + */ + +/** + * PHP 5 Implementation of the Porter Stemmer algorithm. Certain elements + * were borrowed from the (broken) implementation by Jon Abernathy. + * + * See http://tartarus.org/~martin/PorterStemmer/ for a description of the + * algorithm. + * + * Usage: + * + * $stem = PorterStemmer::Stem($word); + * + * How easy is that? + * + * @package PorterStemmer + * @author Richard Heyes + * @author Jon Abernathy + * @copyright 2005-2016 Richard Heyes (http://www.phpguru.org/) + * @license http://www.opensource.org/licenses/mit-license.html MIT License + */ +class Porter +{ + /** + * Regex for matching a consonant + * + * @var string + */ + private static $regex_consonant = '(?:[bcdfghjklmnpqrstvwxz]|(?<=[aeiou])y|^y)'; + + /** + * Regex for matching a vowel + * + * @var string + */ + private static $regex_vowel = '(?:[aeiou]|(? 1) { + self::replace($word, 'e', ''); + + } elseif (self::m(substr($word, 0, -1)) == 1) { + + if (!self::cvc(substr($word, 0, -1))) { + self::replace($word, 'e', ''); + } + } + } + + // Part b + if (self::m($word) > 1 AND self::doubleConsonant($word) AND substr($word, -1) == 'l') { + $word = substr($word, 0, -1); + } + + return $word; + } + + /** + * Replaces the first string with the second, at the end of the string + * + * If third arg is given, then the preceding string must match that m + * count at least. + * + * @param string $str String to check + * @param string $check Ending to check for + * @param string $repl Replacement string + * @param int $m Optional minimum number of m() to meet + * + * @return bool Whether the $check string was at the end of the $str + * string. True does not necessarily mean that it was + * replaced. + */ + private static function replace(&$str, $check, $repl, $m = null) + { + $len = 0 - strlen($check); + + if (substr($str, $len) == $check) { + $substr = substr($str, 0, $len); + if (is_null($m) OR self::m($substr) > $m) { + $str = $substr . $repl; + } + + return true; + } + + return false; + } + + /** + * What, you mean it's not obvious from the name? + * + * m() measures the number of consonant sequences in $str. if c is + * a consonant sequence and v a vowel sequence, and <..> indicates arbitrary + * presence, + * + * gives 0 + * vc gives 1 + * vcvc gives 2 + * vcvcvc gives 3 + * + * @param string $str The string to return the m count for + * + * @return int The m count + */ + private static function m($str) + { + $c = self::$regex_consonant; + $v = self::$regex_vowel; + + $str = preg_replace("#^$c+#", '', $str); + $str = preg_replace("#$v+$#", '', $str); + + preg_match_all("#($v+$c+)#", $str, $matches); + + return count($matches[1]); + } + + /** + * Returns true/false as to whether the given string contains two + * of the same consonant next to each other at the end of the string. + * + * @param string $str String to check + * + * @return bool Result + */ + private static function doubleConsonant($str) + { + $c = self::$regex_consonant; + + return preg_match("#$c{2}$#", $str, $matches) AND $matches[0][0] == $matches[0][1]; + } + + /** + * Checks for ending CVC sequence where second C is not W, X or Y + * + * @param string $str String to check + * + * @return bool Result + */ + private static function cvc($str) + { + $c = self::$regex_consonant; + $v = self::$regex_vowel; + + return preg_match("#($c$v$c)$#", $str, $matches) + AND strlen($matches[1]) == 3 + AND $matches[1][2] != 'w' + AND $matches[1][2] != 'x' + AND $matches[1][2] != 'y'; + } +} diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 23f002e36b..9b8013c826 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'b88ac037', + 'core.pkg.css' => 'ad8fc332', 'core.pkg.js' => '705aec2c', 'differential.pkg.css' => '607c84be', 'differential.pkg.js' => '1b97518d', @@ -112,7 +112,7 @@ return array( 'rsrc/css/application/tokens/tokens.css' => 'ce5a50bd', 'rsrc/css/application/uiexample/example.css' => 'b4795059', 'rsrc/css/core/core.css' => '1b29ed61', - 'rsrc/css/core/remarkup.css' => 'f06cc20e', + 'rsrc/css/core/remarkup.css' => 'c286eaef', 'rsrc/css/core/syntax.css' => '220b85f9', 'rsrc/css/core/z-index.css' => '99c0f5eb', 'rsrc/css/diviner/diviner-shared.css' => '4bd263b0', @@ -153,9 +153,9 @@ return array( 'rsrc/css/phui/phui-feed-story.css' => 'a0c05029', 'rsrc/css/phui/phui-fontkit.css' => '1ec937e5', 'rsrc/css/phui/phui-form-view.css' => '01b796c0', - 'rsrc/css/phui/phui-form.css' => '159e2d9c', + 'rsrc/css/phui/phui-form.css' => '1f177cb7', 'rsrc/css/phui/phui-head-thing.css' => 'd7f293df', - 'rsrc/css/phui/phui-header-view.css' => 'be09cc83', + 'rsrc/css/phui/phui-header-view.css' => '36c86a58', 'rsrc/css/phui/phui-hovercard.css' => '6ca90fa0', 'rsrc/css/phui/phui-icon-set-selector.css' => '7aa5f3ec', 'rsrc/css/phui/phui-icon.css' => '4cbc684a', @@ -176,7 +176,7 @@ return array( 'rsrc/css/phui/phui-status.css' => 'e5ff8be0', 'rsrc/css/phui/phui-tag-view.css' => '8519160a', 'rsrc/css/phui/phui-timeline-view.css' => '1e348e4b', - 'rsrc/css/phui/phui-two-column-view.css' => '01e6991e', + 'rsrc/css/phui/phui-two-column-view.css' => '0a876b9e', 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', 'rsrc/css/phui/workboards/phui-workcard.css' => '913441b6', @@ -794,7 +794,7 @@ return array( 'phabricator-object-selector-css' => 'ee77366f', 'phabricator-phtize' => '2f1db1ed', 'phabricator-prefab' => '5793d835', - 'phabricator-remarkup-css' => 'f06cc20e', + 'phabricator-remarkup-css' => 'c286eaef', 'phabricator-search-results-css' => '9ea70ace', 'phabricator-shaped-request' => 'abf88db8', 'phabricator-slowvote-css' => '1694baed', @@ -840,10 +840,10 @@ return array( 'phui-feed-story-css' => 'a0c05029', 'phui-font-icon-base-css' => 'd7994e06', 'phui-fontkit-css' => '1ec937e5', - 'phui-form-css' => '159e2d9c', + 'phui-form-css' => '1f177cb7', 'phui-form-view-css' => '01b796c0', 'phui-head-thing-view-css' => 'd7f293df', - 'phui-header-view-css' => 'be09cc83', + 'phui-header-view-css' => '36c86a58', 'phui-hovercard' => '074f0783', 'phui-hovercard-view-css' => '6ca90fa0', 'phui-icon-set-selector-css' => '7aa5f3ec', @@ -873,7 +873,7 @@ return array( 'phui-tag-view-css' => '8519160a', 'phui-theme-css' => '35883b37', 'phui-timeline-view-css' => '1e348e4b', - 'phui-two-column-view-css' => '01e6991e', + 'phui-two-column-view-css' => '0a876b9e', 'phui-workboard-color-css' => 'e86de308', 'phui-workboard-view-css' => '74fc9d98', 'phui-workcard-view-css' => '913441b6', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 174c0e4876..9b042e024a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -603,6 +603,7 @@ phutil_register_library_map(array( 'DifferentialRevisionActionTransaction' => 'applications/differential/xaction/DifferentialRevisionActionTransaction.php', 'DifferentialRevisionAffectedFilesHeraldField' => 'applications/differential/herald/DifferentialRevisionAffectedFilesHeraldField.php', 'DifferentialRevisionAuthorHeraldField' => 'applications/differential/herald/DifferentialRevisionAuthorHeraldField.php', + 'DifferentialRevisionAuthorPackagesHeraldField' => 'applications/differential/herald/DifferentialRevisionAuthorPackagesHeraldField.php', 'DifferentialRevisionAuthorProjectsHeraldField' => 'applications/differential/herald/DifferentialRevisionAuthorProjectsHeraldField.php', 'DifferentialRevisionBuildableTransaction' => 'applications/differential/xaction/DifferentialRevisionBuildableTransaction.php', 'DifferentialRevisionCloseDetailsController' => 'applications/differential/controller/DifferentialRevisionCloseDetailsController.php', @@ -736,12 +737,14 @@ phutil_register_library_map(array( 'DiffusionCommitAuditorsHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php', 'DiffusionCommitAuditorsTransaction' => 'applications/diffusion/xaction/DiffusionCommitAuditorsTransaction.php', 'DiffusionCommitAuthorHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php', + 'DiffusionCommitAuthorPackagesHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorPackagesHeraldField.php', 'DiffusionCommitAuthorProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitAuthorProjectsHeraldField.php', 'DiffusionCommitAutocloseHeraldField' => 'applications/diffusion/herald/DiffusionCommitAutocloseHeraldField.php', 'DiffusionCommitBranchesController' => 'applications/diffusion/controller/DiffusionCommitBranchesController.php', 'DiffusionCommitBranchesHeraldField' => 'applications/diffusion/herald/DiffusionCommitBranchesHeraldField.php', 'DiffusionCommitBuildableTransaction' => 'applications/diffusion/xaction/DiffusionCommitBuildableTransaction.php', 'DiffusionCommitCommitterHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterHeraldField.php', + 'DiffusionCommitCommitterPackagesHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterPackagesHeraldField.php', 'DiffusionCommitCommitterProjectsHeraldField' => 'applications/diffusion/herald/DiffusionCommitCommitterProjectsHeraldField.php', 'DiffusionCommitConcernTransaction' => 'applications/diffusion/xaction/DiffusionCommitConcernTransaction.php', 'DiffusionCommitController' => 'applications/diffusion/controller/DiffusionCommitController.php', @@ -907,10 +910,12 @@ phutil_register_library_map(array( 'DiffusionPhpExternalSymbolsSource' => 'applications/diffusion/symbol/DiffusionPhpExternalSymbolsSource.php', 'DiffusionPreCommitContentAffectedFilesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAffectedFilesHeraldField.php', 'DiffusionPreCommitContentAuthorHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorHeraldField.php', + 'DiffusionPreCommitContentAuthorPackagesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorPackagesHeraldField.php', 'DiffusionPreCommitContentAuthorProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorProjectsHeraldField.php', 'DiffusionPreCommitContentAuthorRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentAuthorRawHeraldField.php', 'DiffusionPreCommitContentBranchesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentBranchesHeraldField.php', 'DiffusionPreCommitContentCommitterHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterHeraldField.php', + 'DiffusionPreCommitContentCommitterPackagesHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterPackagesHeraldField.php', 'DiffusionPreCommitContentCommitterProjectsHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterProjectsHeraldField.php', 'DiffusionPreCommitContentCommitterRawHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentCommitterRawHeraldField.php', 'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'applications/diffusion/herald/DiffusionPreCommitContentDiffContentAddedHeraldField.php', @@ -1527,6 +1532,7 @@ phutil_register_library_map(array( 'HeraldApplicationActionGroup' => 'applications/herald/action/HeraldApplicationActionGroup.php', 'HeraldApplyTranscript' => 'applications/herald/storage/transcript/HeraldApplyTranscript.php', 'HeraldBasicFieldGroup' => 'applications/herald/field/HeraldBasicFieldGroup.php', + 'HeraldBoolFieldValue' => 'applications/herald/value/HeraldBoolFieldValue.php', 'HeraldBuildableState' => 'applications/herald/state/HeraldBuildableState.php', 'HeraldCallWebhookAction' => 'applications/herald/action/HeraldCallWebhookAction.php', 'HeraldCommentAction' => 'applications/herald/action/HeraldCommentAction.php', @@ -4382,6 +4388,9 @@ phutil_register_library_map(array( 'PhabricatorProjectSubprojectsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php', 'PhabricatorProjectSubtypeDatasource' => 'applications/project/typeahead/PhabricatorProjectSubtypeDatasource.php', 'PhabricatorProjectSubtypesConfigType' => 'applications/project/config/PhabricatorProjectSubtypesConfigType.php', + 'PhabricatorProjectTagsAddedField' => 'applications/project/herald/PhabricatorProjectTagsAddedField.php', + 'PhabricatorProjectTagsField' => 'applications/project/herald/PhabricatorProjectTagsField.php', + 'PhabricatorProjectTagsRemovedField' => 'applications/project/herald/PhabricatorProjectTagsRemovedField.php', 'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php', 'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php', 'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php', @@ -5658,6 +5667,12 @@ phutil_register_library_map(array( 'PhutilRemarkupTableBlockRule' => 'infrastructure/markup/blockrule/PhutilRemarkupTableBlockRule.php', 'PhutilRemarkupTestInterpreterRule' => 'infrastructure/markup/blockrule/PhutilRemarkupTestInterpreterRule.php', 'PhutilRemarkupUnderlineRule' => 'infrastructure/markup/markuprule/PhutilRemarkupUnderlineRule.php', + 'PhutilSearchQueryCompiler' => 'applications/search/compiler/PhutilSearchQueryCompiler.php', + 'PhutilSearchQueryCompilerSyntaxException' => 'applications/search/compiler/PhutilSearchQueryCompilerSyntaxException.php', + 'PhutilSearchQueryCompilerTestCase' => 'applications/search/compiler/__tests__/PhutilSearchQueryCompilerTestCase.php', + 'PhutilSearchQueryToken' => 'applications/search/compiler/PhutilSearchQueryToken.php', + 'PhutilSearchStemmer' => 'applications/search/compiler/PhutilSearchStemmer.php', + 'PhutilSearchStemmerTestCase' => 'applications/search/compiler/__tests__/PhutilSearchStemmerTestCase.php', 'PhutilSlackAuthAdapter' => 'applications/auth/adapter/PhutilSlackAuthAdapter.php', 'PhutilTwitchAuthAdapter' => 'applications/auth/adapter/PhutilTwitchAuthAdapter.php', 'PhutilTwitterAuthAdapter' => 'applications/auth/adapter/PhutilTwitterAuthAdapter.php', @@ -6586,6 +6601,7 @@ phutil_register_library_map(array( 'DifferentialRevisionActionTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionAffectedFilesHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionAuthorHeraldField' => 'DifferentialRevisionHeraldField', + 'DifferentialRevisionAuthorPackagesHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionAuthorProjectsHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionBuildableTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionCloseDetailsController' => 'DifferentialController', @@ -6719,12 +6735,14 @@ phutil_register_library_map(array( 'DiffusionCommitAuditorsHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAuditorsTransaction' => 'DiffusionCommitTransactionType', 'DiffusionCommitAuthorHeraldField' => 'DiffusionCommitHeraldField', + 'DiffusionCommitAuthorPackagesHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAuthorProjectsHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitAutocloseHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitBranchesController' => 'DiffusionController', 'DiffusionCommitBranchesHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitBuildableTransaction' => 'DiffusionCommitTransactionType', 'DiffusionCommitCommitterHeraldField' => 'DiffusionCommitHeraldField', + 'DiffusionCommitCommitterPackagesHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitCommitterProjectsHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitConcernTransaction' => 'DiffusionCommitAuditTransaction', 'DiffusionCommitController' => 'DiffusionController', @@ -6893,10 +6911,12 @@ phutil_register_library_map(array( 'DiffusionPhpExternalSymbolsSource' => 'DiffusionExternalSymbolsSource', 'DiffusionPreCommitContentAffectedFilesHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentAuthorHeraldField' => 'DiffusionPreCommitContentHeraldField', + 'DiffusionPreCommitContentAuthorPackagesHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentAuthorProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentAuthorRawHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentBranchesHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentCommitterHeraldField' => 'DiffusionPreCommitContentHeraldField', + 'DiffusionPreCommitContentCommitterPackagesHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentCommitterProjectsHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentCommitterRawHeraldField' => 'DiffusionPreCommitContentHeraldField', 'DiffusionPreCommitContentDiffContentAddedHeraldField' => 'DiffusionPreCommitContentHeraldField', @@ -7622,6 +7642,7 @@ phutil_register_library_map(array( 'HeraldApplicationActionGroup' => 'HeraldActionGroup', 'HeraldApplyTranscript' => 'Phobject', 'HeraldBasicFieldGroup' => 'HeraldFieldGroup', + 'HeraldBoolFieldValue' => 'HeraldFieldValue', 'HeraldBuildableState' => 'HeraldState', 'HeraldCallWebhookAction' => 'HeraldAction', 'HeraldCommentAction' => 'HeraldAction', @@ -7673,7 +7694,7 @@ phutil_register_library_map(array( 'HeraldPreCommitContentAdapter' => 'HeraldPreCommitAdapter', 'HeraldPreCommitRefAdapter' => 'HeraldPreCommitAdapter', 'HeraldPreventActionGroup' => 'HeraldActionGroup', - 'HeraldProjectsField' => 'HeraldField', + 'HeraldProjectsField' => 'PhabricatorProjectTagsField', 'HeraldRecursiveConditionsException' => 'Exception', 'HeraldRelatedFieldGroup' => 'HeraldFieldGroup', 'HeraldRemarkupFieldValue' => 'HeraldFieldValue', @@ -10937,6 +10958,9 @@ phutil_register_library_map(array( 'PhabricatorProjectSubprojectsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorProjectSubtypeDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorProjectSubtypesConfigType' => 'PhabricatorJSONConfigType', + 'PhabricatorProjectTagsAddedField' => 'PhabricatorProjectTagsField', + 'PhabricatorProjectTagsField' => 'HeraldField', + 'PhabricatorProjectTagsRemovedField' => 'PhabricatorProjectTagsField', 'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorProjectTransaction' => 'PhabricatorModularTransaction', 'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor', @@ -12483,6 +12507,12 @@ phutil_register_library_map(array( 'PhutilRemarkupTableBlockRule' => 'PhutilRemarkupBlockRule', 'PhutilRemarkupTestInterpreterRule' => 'PhutilRemarkupBlockInterpreter', 'PhutilRemarkupUnderlineRule' => 'PhutilRemarkupRule', + 'PhutilSearchQueryCompiler' => 'Phobject', + 'PhutilSearchQueryCompilerSyntaxException' => 'Exception', + 'PhutilSearchQueryCompilerTestCase' => 'PhutilTestCase', + 'PhutilSearchQueryToken' => 'Phobject', + 'PhutilSearchStemmer' => 'Phobject', + 'PhutilSearchStemmerTestCase' => 'PhutilTestCase', 'PhutilSlackAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter', 'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter', diff --git a/src/applications/auth/data/PhabricatorAuthHighSecurityToken.php b/src/applications/auth/data/PhabricatorAuthHighSecurityToken.php index 8ea1ed97f8..9d44411795 100644 --- a/src/applications/auth/data/PhabricatorAuthHighSecurityToken.php +++ b/src/applications/auth/data/PhabricatorAuthHighSecurityToken.php @@ -1,3 +1,17 @@ isUnchallengedToken = $is_unchallenged_token; + return $this; + } + + public function getIsUnchallengedToken() { + return $this->isUnchallengedToken; + } + +} diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index 7358a61a40..251c8284ef 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -493,7 +493,8 @@ final class PhabricatorAuthSessionEngine extends Phobject { // adds an auth factor, existing sessions won't get a free pass into hisec, // since they never actually got marked as hisec. if (!$factors) { - return $this->issueHighSecurityToken($session, true); + return $this->issueHighSecurityToken($session, true) + ->setIsUnchallengedToken(true); } $this->request = $request; diff --git a/src/applications/differential/herald/DifferentialRevisionAuthorPackagesHeraldField.php b/src/applications/differential/herald/DifferentialRevisionAuthorPackagesHeraldField.php new file mode 100644 index 0000000000..89a7df4cdb --- /dev/null +++ b/src/applications/differential/herald/DifferentialRevisionAuthorPackagesHeraldField.php @@ -0,0 +1,32 @@ +getAdapter(); + $viewer = $adapter->getViewer(); + + $packages = id(new PhabricatorOwnersPackageQuery()) + ->setViewer($viewer) + ->withAuthorityPHIDs(array($object->getAuthorPHID())) + ->execute(); + + return mpull($packages, 'getPHID'); + } + + protected function getHeraldFieldStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorOwnersPackageDatasource(); + } + +} diff --git a/src/applications/differential/parser/DifferentialHunkParser.php b/src/applications/differential/parser/DifferentialHunkParser.php index 6cdadf69e7..924789ca83 100644 --- a/src/applications/differential/parser/DifferentialHunkParser.php +++ b/src/applications/differential/parser/DifferentialHunkParser.php @@ -354,6 +354,69 @@ final class DifferentialHunkParser extends Phobject { return $this; } + public function generateVisibleBlocksMask($lines_context) { + + // See T13468. This is similar to "generateVisibleLinesMask()", but + // attempts to work around a series of bugs which cancel each other + // out but make a mess of the intermediate steps. + + $old = $this->getOldLines(); + $new = $this->getNewLines(); + + $length = max(count($old), count($new)); + + $visible_lines = array(); + for ($ii = 0; $ii < $length; $ii++) { + $old_visible = (isset($old[$ii]) && $old[$ii]['type']); + $new_visible = (isset($new[$ii]) && $new[$ii]['type']); + + $visible_lines[$ii] = ($old_visible || $new_visible); + } + + $mask = array(); + $reveal_cursor = -1; + for ($ii = 0; $ii < $length; $ii++) { + + // If this line isn't visible, it isn't going to reveal anything. + if (!$visible_lines[$ii]) { + + // If it hasn't been revealed by a nearby line, mark it as masked. + if (empty($mask[$ii])) { + $mask[$ii] = false; + } + + continue; + } + + // If this line is visible, reveal all the lines nearby. + + // First, compute the minimum and maximum offsets we want to reveal. + $min_reveal = max($ii - $lines_context, 0); + $max_reveal = min($ii + $lines_context, $length - 1); + + // Naively, we'd do more work than necessary when revealing context for + // several adjacent visible lines: we would mark all the overlapping + // lines as revealed several times. + + // To avoid duplicating work, keep track of the largest line we've + // revealed to. Since we reveal context by marking every consecutive + // line, we don't need to touch any line above it. + $min_reveal = max($min_reveal, $reveal_cursor); + + // Reveal the remaining unrevealed lines. + for ($jj = $min_reveal; $jj <= $max_reveal; $jj++) { + $mask[$jj] = true; + } + + // Move the cursor to the next line which may still need to be revealed. + $reveal_cursor = $max_reveal + 1; + } + + $this->setVisibleLinesMask($mask); + + return $mask; + } + public function generateVisibleLinesMask($lines_context) { $old = $this->getOldLines(); $new = $this->getNewLines(); @@ -361,6 +424,7 @@ final class DifferentialHunkParser extends Phobject { $visible = false; $last = 0; $mask = array(); + for ($cursor = -$lines_context; $cursor < $max_length; $cursor++) { $offset = $cursor + $lines_context; if ((isset($old[$offset]) && $old[$offset]['type']) || diff --git a/src/applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php index ba1b636684..4c6fd74b31 100644 --- a/src/applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionCommitAuthorHeraldField.php @@ -10,7 +10,7 @@ final class DiffusionCommitAuthorHeraldField } public function getHeraldFieldValue($object) { - return $object->getCommitData()->getCommitDetail('authorPHID'); + return $this->getAdapter()->getAuthorPHID(); } protected function getHeraldFieldStandardType() { diff --git a/src/applications/diffusion/herald/DiffusionCommitAuthorPackagesHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitAuthorPackagesHeraldField.php new file mode 100644 index 0000000000..ec4e89c716 --- /dev/null +++ b/src/applications/diffusion/herald/DiffusionCommitAuthorPackagesHeraldField.php @@ -0,0 +1,37 @@ +getAdapter(); + $viewer = $adapter->getViewer(); + + $author_phid = $adapter->getAuthorPHID(); + if (!$author_phid) { + return array(); + } + + $packages = id(new PhabricatorOwnersPackageQuery()) + ->setViewer($viewer) + ->withAuthorityPHIDs(array($author_phid)) + ->execute(); + + return mpull($packages, 'getPHID'); + } + + protected function getHeraldFieldStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorOwnersPackageDatasource(); + } + +} diff --git a/src/applications/diffusion/herald/DiffusionCommitAuthorProjectsHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitAuthorProjectsHeraldField.php index a90480d50a..d61423edbc 100644 --- a/src/applications/diffusion/herald/DiffusionCommitAuthorProjectsHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionCommitAuthorProjectsHeraldField.php @@ -11,17 +11,16 @@ final class DiffusionCommitAuthorProjectsHeraldField public function getHeraldFieldValue($object) { $adapter = $this->getAdapter(); + $viewer = $adapter->getViewer(); - $phid = $object->getCommitData()->getCommitDetail('authorPHID'); - if (!$phid) { + $author_phid = $adapter->getAuthorPHID(); + if (!$author_phid) { return array(); } - $viewer = $adapter->getViewer(); - $projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) - ->withMemberPHIDs(array($phid)) + ->withMemberPHIDs(array($author_phid)) ->execute(); return mpull($projects, 'getPHID'); diff --git a/src/applications/diffusion/herald/DiffusionCommitCommitterHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitCommitterHeraldField.php index 825648c46c..6f06cad07b 100644 --- a/src/applications/diffusion/herald/DiffusionCommitCommitterHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionCommitCommitterHeraldField.php @@ -10,7 +10,7 @@ final class DiffusionCommitCommitterHeraldField } public function getHeraldFieldValue($object) { - return $object->getCommitData()->getCommitDetail('committerPHID'); + return $this->getAdapter()->getCommitterPHID(); } protected function getHeraldFieldStandardType() { diff --git a/src/applications/diffusion/herald/DiffusionCommitCommitterPackagesHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitCommitterPackagesHeraldField.php new file mode 100644 index 0000000000..5ef5824b63 --- /dev/null +++ b/src/applications/diffusion/herald/DiffusionCommitCommitterPackagesHeraldField.php @@ -0,0 +1,37 @@ +getAdapter(); + $viewer = $adapter->getViewer(); + + $committer_phid = $adapter->getAuthorPHID(); + if (!$committer_phid) { + return array(); + } + + $packages = id(new PhabricatorOwnersPackageQuery()) + ->setViewer($viewer) + ->withAuthorityPHIDs(array($committer_phid)) + ->execute(); + + return mpull($packages, 'getPHID'); + } + + protected function getHeraldFieldStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorOwnersPackageDatasource(); + } + +} diff --git a/src/applications/diffusion/herald/DiffusionCommitCommitterProjectsHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitCommitterProjectsHeraldField.php index 97e604a481..5f16a29f82 100644 --- a/src/applications/diffusion/herald/DiffusionCommitCommitterProjectsHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionCommitCommitterProjectsHeraldField.php @@ -11,17 +11,16 @@ final class DiffusionCommitCommitterProjectsHeraldField public function getHeraldFieldValue($object) { $adapter = $this->getAdapter(); + $viewer = $adapter->getViewer(); - $phid = $object->getCommitData()->getCommitDetail('committerPHID'); - if (!$phid) { + $committer_phid = $adapter->getCommitterPHID(); + if (!$committer_phid) { return array(); } - $viewer = $adapter->getViewer(); - $projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) - ->withMemberPHIDs(array($phid)) + ->withMemberPHIDs(array($committer_phid)) ->execute(); return mpull($projects, 'getPHID'); diff --git a/src/applications/diffusion/herald/DiffusionPreCommitContentAuthorPackagesHeraldField.php b/src/applications/diffusion/herald/DiffusionPreCommitContentAuthorPackagesHeraldField.php new file mode 100644 index 0000000000..9832958faf --- /dev/null +++ b/src/applications/diffusion/herald/DiffusionPreCommitContentAuthorPackagesHeraldField.php @@ -0,0 +1,37 @@ +getAdapter(); + $viewer = $adapter->getViewer(); + + $author_phid = $adapter->getAuthorPHID(); + if (!$author_phid) { + return array(); + } + + $packages = id(new PhabricatorOwnersPackageQuery()) + ->setViewer($viewer) + ->withAuthorityPHIDs(array($author_phid)) + ->execute(); + + return mpull($packages, 'getPHID'); + } + + protected function getHeraldFieldStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorOwnersPackageDatasource(); + } + +} diff --git a/src/applications/diffusion/herald/DiffusionPreCommitContentAuthorProjectsHeraldField.php b/src/applications/diffusion/herald/DiffusionPreCommitContentAuthorProjectsHeraldField.php index ec138c9bde..2d0268deba 100644 --- a/src/applications/diffusion/herald/DiffusionPreCommitContentAuthorProjectsHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionPreCommitContentAuthorProjectsHeraldField.php @@ -11,17 +11,16 @@ final class DiffusionPreCommitContentAuthorProjectsHeraldField public function getHeraldFieldValue($object) { $adapter = $this->getAdapter(); + $viewer = $adapter->getViewer(); - $phid = $adapter->getAuthorPHID(); - if (!$phid) { + $author_phid = $adapter->getAuthorPHID(); + if (!$author_phid) { return array(); } - $viewer = $adapter->getViewer(); - $projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) - ->withMemberPHIDs(array($phid)) + ->withMemberPHIDs(array($author_phid)) ->execute(); return mpull($projects, 'getPHID'); diff --git a/src/applications/diffusion/herald/DiffusionPreCommitContentCommitterPackagesHeraldField.php b/src/applications/diffusion/herald/DiffusionPreCommitContentCommitterPackagesHeraldField.php new file mode 100644 index 0000000000..cd4674ef07 --- /dev/null +++ b/src/applications/diffusion/herald/DiffusionPreCommitContentCommitterPackagesHeraldField.php @@ -0,0 +1,37 @@ +getAdapter(); + $viewer = $adapter->getViewer(); + + $committer_phid = $adapter->getCommitterPHID(); + if (!$committer_phid) { + return array(); + } + + $packages = id(new PhabricatorOwnersPackageQuery()) + ->setViewer($viewer) + ->withAuthorityPHIDs(array($committer_phid)) + ->execute(); + + return mpull($packages, 'getPHID'); + } + + protected function getHeraldFieldStandardType() { + return self::STANDARD_PHID_LIST; + } + + protected function getDatasource() { + return new PhabricatorOwnersPackageDatasource(); + } + +} diff --git a/src/applications/diffusion/herald/DiffusionPreCommitContentCommitterProjectsHeraldField.php b/src/applications/diffusion/herald/DiffusionPreCommitContentCommitterProjectsHeraldField.php index 9b0f012f3c..109649241f 100644 --- a/src/applications/diffusion/herald/DiffusionPreCommitContentCommitterProjectsHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionPreCommitContentCommitterProjectsHeraldField.php @@ -11,17 +11,16 @@ final class DiffusionPreCommitContentCommitterProjectsHeraldField public function getHeraldFieldValue($object) { $adapter = $this->getAdapter(); + $viewer = $adapter->getViewer(); - $phid = $adapter->getCommitterPHID(); - if (!$phid) { + $committer_phid = $adapter->getCommitterPHID(); + if (!$committer_phid) { return array(); } - $viewer = $adapter->getViewer(); - $projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) - ->withMemberPHIDs(array($phid)) + ->withMemberPHIDs(array($committer_phid)) ->execute(); return mpull($projects, 'getPHID'); diff --git a/src/applications/diffusion/herald/HeraldCommitAdapter.php b/src/applications/diffusion/herald/HeraldCommitAdapter.php index 1755cb7701..d285d23f9b 100644 --- a/src/applications/diffusion/herald/HeraldCommitAdapter.php +++ b/src/applications/diffusion/herald/HeraldCommitAdapter.php @@ -35,18 +35,6 @@ final class HeraldCommitAdapter } public function newTestAdapter(PhabricatorUser $viewer, $object) { - $object = id(new DiffusionCommitQuery()) - ->setViewer($viewer) - ->withPHIDs(array($object->getPHID())) - ->needCommitData(true) - ->executeOne(); - if (!$object) { - throw new Exception( - pht( - 'Failed to reload commit ("%s") to fetch commit data.', - $object->getPHID())); - } - return id(clone $this) ->setObject($object); } @@ -56,7 +44,23 @@ final class HeraldCommitAdapter } public function setObject($object) { - $this->commit = $object; + $viewer = $this->getViewer(); + $commit_phid = $object->getPHID(); + + $commit = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->withPHIDs(array($commit_phid)) + ->needCommitData(true) + ->needIdentities(true) + ->executeOne(); + if (!$commit) { + throw new Exception( + pht( + 'Failed to reload commit ("%s") to fetch commit data.', + $commit_phid)); + } + + $this->commit = $commit; return $this; } @@ -352,6 +356,22 @@ final class HeraldCommitAdapter return $this->getObject()->getRepository(); } + public function getAuthorPHID() { + return $this->getObject()->getEffectiveAuthorPHID(); + } + + public function getCommitterPHID() { + $commit = $this->getObject(); + + if ($commit->hasCommitterIdentity()) { + $identity = $commit->getCommitterIdentity(); + return $identity->getCurrentEffectiveUserPHID(); + } + + return null; + } + + /* -( HarbormasterBuildableAdapterInterface )------------------------------ */ diff --git a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php index cee5f97419..7e56074303 100644 --- a/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php +++ b/src/applications/diffusion/herald/HeraldPreCommitContentAdapter.php @@ -8,6 +8,7 @@ final class HeraldPreCommitContentAdapter extends HeraldPreCommitAdapter { private $revision = false; private $affectedPackages; + private $identityCache = array(); public function getAdapterContentName() { return pht('Commit Hook: Commit Content'); @@ -166,10 +167,39 @@ final class HeraldPreCommitContentAdapter extends HeraldPreCommitAdapter { } } - private function lookupUser($author) { - return id(new DiffusionResolveUserQuery()) - ->withName($author) - ->execute(); + private function lookupUser($raw_identity) { + // See T13480. After the move to repository identities, we want to look + // users up in the identity table. If you push a commit which is authored + // by "A Duck " and that identity is bound to user + // "@mallard" in the identity table, Herald should see the author of the + // commit as "@mallard" when evaluating pre-commit content rules. + + if (!array_key_exists($raw_identity, $this->identityCache)) { + $repository = $this->getHookEngine()->getRepository(); + $viewer = $this->getHookEngine()->getViewer(); + + $identity_engine = id(new DiffusionRepositoryIdentityEngine()) + ->setViewer($viewer); + + // We must provide a "sourcePHID" when resolving identities, but don't + // have a legitimate one yet. Just use the repository PHID as a + // reasonable value. This won't actually be written to storage. + $source_phid = $repository->getPHID(); + $identity_engine->setSourcePHID($source_phid); + + // If the identity doesn't exist yet, we don't want to create it if + // we haven't seen it before. It will be created later when we actually + // import the commit. + $identity_engine->setDryRun(true); + + $author_identity = $identity_engine->newResolvedIdentity($raw_identity); + + $effective_phid = $author_identity->getCurrentEffectiveUserPHID(); + + $this->identityCache[$raw_identity] = $effective_phid; + } + + return $this->identityCache[$raw_identity]; } private function getCommitFields() { diff --git a/src/applications/files/diff/PhabricatorDocumentEngineBlocks.php b/src/applications/files/diff/PhabricatorDocumentEngineBlocks.php index 47ddf194ee..2847b53c0d 100644 --- a/src/applications/files/diff/PhabricatorDocumentEngineBlocks.php +++ b/src/applications/files/diff/PhabricatorDocumentEngineBlocks.php @@ -59,7 +59,7 @@ final class PhabricatorDocumentEngineBlocks ->parseHunksForLineData($changeset->getHunks()) ->reparseHunksForSpecialAttributes(); - $hunk_parser->generateVisibleLinesMask(2); + $hunk_parser->generateVisibleBlocksMask(2); $mask = $hunk_parser->getVisibleLinesMask(); $old_lines = $hunk_parser->getOldLines(); @@ -72,14 +72,7 @@ final class PhabricatorDocumentEngineBlocks $old_line = idx($old_lines, $ii); $new_line = idx($new_lines, $ii); - $is_visible = !empty($mask[$ii + 1]); - - // TODO: There's currently a bug where one-line files get incorrectly - // masked. This causes images to completely fail to render. Just ignore - // the mask if it came back empty. - if (!$mask) { - $is_visible = true; - } + $is_visible = !empty($mask[$ii]); if ($old_line) { $old_hash = rtrim($old_line['text'], "\n"); diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index 70f6e3d5cb..76955751c3 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -942,7 +942,6 @@ abstract class HeraldAdapter extends Phobject { public function renderRuleAsText( HeraldRule $rule, - PhabricatorHandleList $handles, PhabricatorUser $viewer) { require_celerity_resource('herald-css'); @@ -973,7 +972,7 @@ abstract class HeraldAdapter extends Phobject { ), array( $icon, - $this->renderConditionAsText($condition, $handles, $viewer), + $this->renderConditionAsText($condition, $viewer), )); } @@ -1004,7 +1003,7 @@ abstract class HeraldAdapter extends Phobject { ), array( $icon, - $this->renderActionAsText($viewer, $action, $handles), + $this->renderActionAsText($viewer, $action), )); } @@ -1018,7 +1017,6 @@ abstract class HeraldAdapter extends Phobject { private function renderConditionAsText( HeraldCondition $condition, - PhabricatorHandleList $handles, PhabricatorUser $viewer) { $field_type = $condition->getFieldName(); @@ -1033,7 +1031,7 @@ abstract class HeraldAdapter extends Phobject { $condition_type = $condition->getFieldCondition(); $condition_name = idx($this->getConditionNameMap(), $condition_type); - $value = $this->renderConditionValueAsText($condition, $handles, $viewer); + $value = $this->renderConditionValueAsText($condition, $viewer); return array( $field_name, @@ -1046,36 +1044,23 @@ abstract class HeraldAdapter extends Phobject { private function renderActionAsText( PhabricatorUser $viewer, - HeraldActionRecord $action, - PhabricatorHandleList $handles) { + HeraldActionRecord $action_record) { - $impl = $this->getActionImplementation($action->getAction()); - if ($impl) { - $impl->setViewer($viewer); + $action_type = $action_record->getAction(); + $action_value = $action_record->getTarget(); - $value = $action->getTarget(); - return $impl->renderActionDescription($value); + $action = $this->getActionImplementation($action_type); + if (!$action) { + return pht('Unknown Action ("%s")', $action_type); } - $rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL; + $action->setViewer($viewer); - $action_type = $action->getAction(); - - $default = pht('(Unknown Action "%s") equals', $action_type); - - $action_name = idx( - $this->getActionNameMap($rule_global), - $action_type, - $default); - - $target = $this->renderActionTargetAsText($action, $handles); - - return hsprintf(' %s %s', $action_name, $target); + return $action->renderActionDescription($action_value); } private function renderConditionValueAsText( HeraldCondition $condition, - PhabricatorHandleList $handles, PhabricatorUser $viewer) { $field = $this->requireFieldImplementation($condition->getFieldName()); @@ -1086,76 +1071,26 @@ abstract class HeraldAdapter extends Phobject { $condition->getValue()); } - private function renderActionTargetAsText( - HeraldActionRecord $action, - PhabricatorHandleList $handles) { + public function renderFieldTranscriptValue( + PhabricatorUser $viewer, + $field_type, + $field_value) { - // TODO: This should be driven through HeraldAction. + $field = $this->getFieldImplementation($field_type); + if ($field) { + return $field->renderTranscriptValue( + $viewer, + $field_value); + } - $target = $action->getTarget(); - if (!is_array($target)) { - $target = array($target); - } - foreach ($target as $index => $val) { - switch ($action->getAction()) { - default: - $handle = $handles->getHandleIfExists($val); - if ($handle) { - $target[$index] = $handle->renderLink(); - } - break; - } - } - $target = phutil_implode_html(', ', $target); - return $target; + return phutil_tag( + 'em', + array(), + pht( + 'Unable to render value for unknown field type ("%s").', + $field_type)); } - /** - * Given a @{class:HeraldRule}, this function extracts all the phids that - * we'll want to load as handles later. - * - * This function performs a somewhat hacky approach to figuring out what - * is and is not a phid - try to get the phid type and if the type is - * *not* unknown assume its a valid phid. - * - * Don't try this at home. Use more strongly typed data at home. - * - * Think of the children. - */ - public static function getHandlePHIDs(HeraldRule $rule) { - $phids = array($rule->getAuthorPHID()); - foreach ($rule->getConditions() as $condition) { - $value = $condition->getValue(); - if (!is_array($value)) { - $value = array($value); - } - foreach ($value as $val) { - if (phid_get_type($val) != - PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { - $phids[] = $val; - } - } - } - - foreach ($rule->getActions() as $action) { - $target = $action->getTarget(); - if (!is_array($target)) { - $target = array($target); - } - foreach ($target as $val) { - if (phid_get_type($val) != - PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { - $phids[] = $val; - } - } - } - - if ($rule->isObjectRule()) { - $phids[] = $rule->getTriggerObjectPHID(); - } - - return $phids; - } /* -( Applying Effects )--------------------------------------------------- */ diff --git a/src/applications/herald/controller/HeraldRuleViewController.php b/src/applications/herald/controller/HeraldRuleViewController.php index 70a2d20224..49e0fd8e32 100644 --- a/src/applications/herald/controller/HeraldRuleViewController.php +++ b/src/applications/herald/controller/HeraldRuleViewController.php @@ -143,12 +143,11 @@ final class HeraldRuleViewController extends HeraldController { private function buildDescriptionView(HeraldRule $rule) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) - ->setUser($viewer); + ->setViewer($viewer); $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType()); if ($adapter) { - $handles = $viewer->loadHandles(HeraldAdapter::getHandlePHIDs($rule)); - $rule_text = $adapter->renderRuleAsText($rule, $handles, $viewer); + $rule_text = $adapter->renderRuleAsText($rule, $viewer); $view->addTextContent($rule_text); return $view; } diff --git a/src/applications/herald/controller/HeraldTranscriptController.php b/src/applications/herald/controller/HeraldTranscriptController.php index 09a23408d3..6cbe4bd808 100644 --- a/src/applications/herald/controller/HeraldTranscriptController.php +++ b/src/applications/herald/controller/HeraldTranscriptController.php @@ -447,52 +447,64 @@ final class HeraldTranscriptController extends HeraldController { } private function buildObjectTranscriptPanel(HeraldTranscript $xscript) { - + $viewer = $this->getViewer(); $adapter = $this->getAdapter(); + $field_names = $adapter->getFieldNameMap(); $object_xscript = $xscript->getObjectTranscript(); - $data = array(); + $rows = array(); if ($object_xscript) { $phid = $object_xscript->getPHID(); $handles = $this->handles; - $data += array( - pht('Object Name') => $object_xscript->getName(), - pht('Object Type') => $object_xscript->getType(), - pht('Object PHID') => $phid, - pht('Object Link') => $handles[$phid]->renderLink(), + $rows[] = array( + pht('Object Name'), + $object_xscript->getName(), + ); + + $rows[] = array( + pht('Object Type'), + $object_xscript->getType(), + ); + + $rows[] = array( + pht('Object PHID'), + $phid, + ); + + $rows[] = array( + pht('Object Link'), + $handles[$phid]->renderLink(), ); } - $data += $xscript->getMetadataMap(); - - if ($object_xscript) { - foreach ($object_xscript->getFields() as $field => $value) { - $field = idx($field_names, $field, '['.$field.'?]'); - $data['Field: '.$field] = $value; - } + foreach ($xscript->getMetadataMap() as $key => $value) { + $rows[] = array( + $key, + $value, + ); } - $rows = array(); - foreach ($data as $name => $value) { - if (!($value instanceof PhutilSafeHTML)) { - if (!is_scalar($value) && !is_null($value)) { - $value = implode("\n", $value); + if ($object_xscript) { + foreach ($object_xscript->getFields() as $field_type => $value) { + if (isset($field_names[$field_type])) { + $field_name = pht('Field: %s', $field_names[$field_type]); + } else { + $field_name = pht('Unknown Field ("%s")', $field_type); } - if (strlen($value) > 256) { - $value = phutil_tag( - 'textarea', - array( - 'class' => 'herald-field-value-transcript', - ), - $value); - } + $field_value = $adapter->renderFieldTranscriptValue( + $viewer, + $field_type, + $value); + + $rows[] = array( + $field_name, + $field_value, + ); } - - $rows[] = array($name, $value); } $property_list = new PHUIPropertyListView(); diff --git a/src/applications/herald/editor/HeraldRuleEditor.php b/src/applications/herald/editor/HeraldRuleEditor.php index 1039d7432a..ea099cc6f3 100644 --- a/src/applications/herald/editor/HeraldRuleEditor.php +++ b/src/applications/herald/editor/HeraldRuleEditor.php @@ -30,6 +30,12 @@ final class HeraldRuleEditor return true; } + public function getTransactionTypes() { + $types = parent::getTransactionTypes(); + $types[] = PhabricatorTransactions::TYPE_EDGE; + return $types; + } + protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); diff --git a/src/applications/herald/field/HeraldAlwaysField.php b/src/applications/herald/field/HeraldAlwaysField.php index 13d3649ef2..bcc7a10e37 100644 --- a/src/applications/herald/field/HeraldAlwaysField.php +++ b/src/applications/herald/field/HeraldAlwaysField.php @@ -23,7 +23,7 @@ final class HeraldAlwaysField extends HeraldField { } public function getHeraldFieldValueType($condition) { - return new HeraldEmptyFieldValue(); + return new HeraldBoolFieldValue(); } public function supportsObject($object) { diff --git a/src/applications/herald/field/HeraldField.php b/src/applications/herald/field/HeraldField.php index 2a1e89d558..cdfdd7e518 100644 --- a/src/applications/herald/field/HeraldField.php +++ b/src/applications/herald/field/HeraldField.php @@ -105,11 +105,16 @@ abstract class HeraldField extends Phobject { } public function getHeraldFieldValueType($condition) { + + // NOTE: The condition type may be "null" to indicate that the caller + // wants a generic field value type. This is used when rendering field + // values in the object transcript. + $standard_type = $this->getHeraldFieldStandardType(); switch ($standard_type) { case self::STANDARD_BOOL: case self::STANDARD_PHID_BOOL: - return new HeraldEmptyFieldValue(); + return new HeraldBoolFieldValue(); case self::STANDARD_TEXT: case self::STANDARD_TEXT_LIST: case self::STANDARD_TEXT_MAP: @@ -176,6 +181,14 @@ abstract class HeraldField extends Phobject { return $value_type->renderEditorValue($value); } + public function renderTranscriptValue( + PhabricatorUser $viewer, + $field_value) { + $value_type = $this->getHeraldFieldValueType($condition_type = null); + $value_type->setViewer($viewer); + return $value_type->renderTranscriptValue($field_value); + } + public function getPHIDsAffectedByCondition(HeraldCondition $condition) { try { $standard_type = $this->getHeraldFieldStandardType(); @@ -241,6 +254,51 @@ abstract class HeraldField extends Phobject { return false; } + final protected function getAppliedTransactionsOfTypes(array $types) { + $types = array_fuse($types); + $xactions = $this->getAdapter()->getAppliedTransactions(); + + $result = array(); + foreach ($xactions as $key => $xaction) { + $xaction_type = $xaction->getTransactionType(); + if (isset($types[$xaction_type])) { + $result[$key] = $xaction; + } + } + + return $result; + } + + final protected function getAppliedEdgeTransactionOfType($edge_type) { + $edge_xactions = $this->getAppliedTransactionsOfTypes( + array( + PhabricatorTransactions::TYPE_EDGE, + )); + + $results = array(); + foreach ($edge_xactions as $edge_xaction) { + $xaction_edge_type = $edge_xaction->getMetadataValue('edge:type'); + if ($xaction_edge_type == $edge_type) { + $results[] = $edge_xaction; + } + } + + if (count($results) > 1) { + throw new Exception( + pht( + 'Found more than one ("%s") applied edge transactions with given '. + 'edge type ("%s"); expected zero or one.', + phutil_count($results), + $edge_type)); + } + + if ($results) { + return head($results); + } + + return null; + } + public function isFieldAvailable() { return true; } diff --git a/src/applications/herald/value/HeraldBoolFieldValue.php b/src/applications/herald/value/HeraldBoolFieldValue.php new file mode 100644 index 0000000000..83acf5aadf --- /dev/null +++ b/src/applications/herald/value/HeraldBoolFieldValue.php @@ -0,0 +1,30 @@ +renderFieldValue($value); + } + } diff --git a/src/applications/herald/value/HeraldTextFieldValue.php b/src/applications/herald/value/HeraldTextFieldValue.php index fd032e33c0..4a9780fd0e 100644 --- a/src/applications/herald/value/HeraldTextFieldValue.php +++ b/src/applications/herald/value/HeraldTextFieldValue.php @@ -19,4 +19,25 @@ final class HeraldTextFieldValue return $value; } + public function renderTranscriptValue($value) { + if (is_array($value)) { + $value = implode('', $value); + } + + if (!strlen($value)) { + return phutil_tag('em', array(), pht('None')); + } + + if (strlen($value) > 256) { + $value = phutil_tag( + 'textarea', + array( + 'class' => 'herald-field-value-transcript', + ), + $value); + } + + return $value; + } + } diff --git a/src/applications/herald/value/HeraldTokenizerFieldValue.php b/src/applications/herald/value/HeraldTokenizerFieldValue.php index 215df29693..4f90267112 100644 --- a/src/applications/herald/value/HeraldTokenizerFieldValue.php +++ b/src/applications/herald/value/HeraldTokenizerFieldValue.php @@ -64,17 +64,7 @@ final class HeraldTokenizerFieldValue } public function renderFieldValue($value) { - $viewer = $this->getViewer(); - $value = (array)$value; - - if ($this->valueMap !== null) { - foreach ($value as $k => $v) { - $value[$k] = idx($this->valueMap, $v, $v); - } - return implode(', ', $value); - } - - return $viewer->renderHandleList((array)$value)->setAsInline(true); + return $this->renderValueAsList($value, $for_transcript = false); } public function renderEditorValue($value) { @@ -87,4 +77,33 @@ final class HeraldTokenizerFieldValue return $datasource->getWireTokens($value); } + public function renderTranscriptValue($value) { + return $this->renderValueAsList($value, $for_transcript = true); + } + + private function renderValueAsList($value, $for_transcript) { + $viewer = $this->getViewer(); + $value = (array)$value; + + if (!$value) { + return phutil_tag('em', array(), pht('None')); + } + + if ($this->valueMap !== null) { + foreach ($value as $k => $v) { + $value[$k] = idx($this->valueMap, $v, $v); + } + + return implode(', ', $value); + } + + $list = $viewer->renderHandleList($value); + + if (!$for_transcript) { + $list->setAsInline(true); + } + + return $list; + } + } diff --git a/src/applications/project/herald/HeraldProjectsField.php b/src/applications/project/herald/HeraldProjectsField.php index 7f66a00cf3..504bde8967 100644 --- a/src/applications/project/herald/HeraldProjectsField.php +++ b/src/applications/project/herald/HeraldProjectsField.php @@ -1,6 +1,7 @@ getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); } - protected function getHeraldFieldStandardType() { - return self::STANDARD_PHID_LIST; - } - - protected function getDatasource() { - return new PhabricatorProjectDatasource(); - } - } diff --git a/src/applications/project/herald/PhabricatorProjectTagsAddedField.php b/src/applications/project/herald/PhabricatorProjectTagsAddedField.php new file mode 100644 index 0000000000..61a974aba2 --- /dev/null +++ b/src/applications/project/herald/PhabricatorProjectTagsAddedField.php @@ -0,0 +1,23 @@ +getProjectTagsTransaction(); + if (!$xaction) { + return array(); + } + + $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction); + + return $record->getAddedPHIDs(); + } + +} diff --git a/src/applications/project/herald/PhabricatorProjectTagsField.php b/src/applications/project/herald/PhabricatorProjectTagsField.php new file mode 100644 index 0000000000..36ea103b00 --- /dev/null +++ b/src/applications/project/herald/PhabricatorProjectTagsField.php @@ -0,0 +1,27 @@ +getAppliedEdgeTransactionOfType( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); + } + +} diff --git a/src/applications/project/herald/PhabricatorProjectTagsRemovedField.php b/src/applications/project/herald/PhabricatorProjectTagsRemovedField.php new file mode 100644 index 0000000000..7e249c266b --- /dev/null +++ b/src/applications/project/herald/PhabricatorProjectTagsRemovedField.php @@ -0,0 +1,23 @@ +getProjectTagsTransaction(); + if (!$xaction) { + return array(); + } + + $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction); + + return $record->getRemovedPHIDs(); + } + +} diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php index 2b008b630b..7d216421d7 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -275,11 +275,39 @@ final class PhabricatorRepositoryPullEngine private function executeGitUpdate() { $repository = $this->getRepository(); + // See T13479. We previously used "--show-toplevel", but this stopped + // working in Git 2.25.0 when run in a bare repository. + + // NOTE: As of Git 2.21.1, "git rev-parse" can not parse "--" in its + // argument list, so we can not specify arguments unambiguously. Any + // version of Git which does not recognize the "--git-dir" flag will + // treat this as a request to parse the literal refname "--git-dir". + list($err, $stdout) = $repository->execLocalCommand( - 'rev-parse --show-toplevel'); + 'rev-parse --git-dir'); + + $repository_root = null; + $path = $repository->getLocalPath(); + + if (!$err) { + $repository_root = Filesystem::resolvePath( + rtrim($stdout, "\n"), + $path); + + // If we're in a bare Git repository, the "--git-dir" will be the + // root directory. If we're in a working copy, the "--git-dir" will + // be the ".git/" directory. + + // Test if the result is the root directory. If it is, we're in good + // shape and appear to be inside a bare repository. If not, take the + // parent directory to get out of the ".git/" folder. + + if (!Filesystem::pathsAreEquivalent($repository_root, $path)) { + $repository_root = dirname($repository_root); + } + } $message = null; - $path = $repository->getLocalPath(); if ($err) { // Try to raise a more tailored error message in the more common case // of the user creating an empty directory. (We could try to remove it, @@ -313,15 +341,14 @@ final class PhabricatorRepositoryPullEngine $path); } } else { - $repo_path = rtrim($stdout, "\n"); - if (empty($repo_path)) { - // This can mean one of two things: we're in a bare repository, or - // we're inside a git repository inside another git repository. Since - // the first is dramatically more likely now that we perform bare - // clones and I don't have a great way to test for the latter, assume - // we're OK. - } else if (!Filesystem::pathsAreEquivalent($repo_path, $path)) { + // Prior to Git 2.25.0, we used "--show-toplevel", which had a weird + // case here when the working copy was inside another working copy. + // The switch to "--git-dir" seems to have resolved this; we now seem + // to find the nearest git directory and thus the correct repository + // root. + + if (!Filesystem::pathsAreEquivalent($repository_root, $path)) { $err = true; $message = pht( 'Expected to find a Git repository at "%s", but the actual Git '. @@ -329,7 +356,7 @@ final class PhabricatorRepositoryPullEngine 'misconfigured. This directory should be writable by the daemons '. 'and not inside another Git repository.', $path, - $repo_path); + $repository_root); } } diff --git a/src/applications/search/compiler/PhutilSearchQueryCompiler.php b/src/applications/search/compiler/PhutilSearchQueryCompiler.php new file mode 100644 index 0000000000..eb5ee860c9 --- /dev/null +++ b/src/applications/search/compiler/PhutilSearchQueryCompiler.php @@ -0,0 +1,374 @@ +<()~*:""&|'; + private $query; + private $stemmer; + private $enableFunctions = false; + + const OPERATOR_NOT = 'not'; + const OPERATOR_AND = 'and'; + const OPERATOR_SUBSTRING = 'sub'; + const OPERATOR_EXACT = 'exact'; + + public function setOperators($operators) { + $this->operators = $operators; + return $this; + } + + public function getOperators() { + return $this->operators; + } + + public function setStemmer(PhutilSearchStemmer $stemmer) { + $this->stemmer = $stemmer; + return $this; + } + + public function getStemmer() { + return $this->stemmer; + } + + public function setEnableFunctions($enable_functions) { + $this->enableFunctions = $enable_functions; + return $this; + } + + public function getEnableFunctions() { + return $this->enableFunctions; + } + + public function compileQuery(array $tokens) { + assert_instances_of($tokens, 'PhutilSearchQueryToken'); + + $result = array(); + foreach ($tokens as $token) { + $result[] = $this->renderToken($token); + } + + return $this->compileRenderedTokens($result); + } + + public function compileLiteralQuery(array $tokens) { + assert_instances_of($tokens, 'PhutilSearchQueryToken'); + + $result = array(); + foreach ($tokens as $token) { + if (!$token->isQuoted()) { + continue; + } + $result[] = $this->renderToken($token); + } + + return $this->compileRenderedTokens($result); + } + + public function compileStemmedQuery(array $tokens) { + assert_instances_of($tokens, 'PhutilSearchQueryToken'); + + $result = array(); + foreach ($tokens as $token) { + if ($token->isQuoted()) { + continue; + } + $result[] = $this->renderToken($token, $this->getStemmer()); + } + + return $this->compileRenderedTokens($result); + } + + private function compileRenderedTokens(array $list) { + if (!$list) { + return null; + } + + $list = array_unique($list); + return implode(' ', $list); + } + + public function newTokens($query) { + $results = $this->tokenizeQuery($query); + + $tokens = array(); + foreach ($results as $result) { + $tokens[] = PhutilSearchQueryToken::newFromDictionary($result); + } + + return $tokens; + } + + private function tokenizeQuery($query) { + $maximum_bytes = 1024; + + $query_bytes = strlen($query); + if ($query_bytes > $maximum_bytes) { + throw new PhutilSearchQueryCompilerSyntaxException( + pht( + 'Query is too long (%s bytes, maximum is %s bytes).', + new PhutilNumber($query_bytes), + new PhutilNumber($maximum_bytes))); + } + + $query = phutil_utf8v($query); + $length = count($query); + + $enable_functions = $this->getEnableFunctions(); + + $mode = 'scan'; + $current_operator = array(); + $current_token = array(); + $current_function = null; + $is_quoted = false; + $tokens = array(); + + if ($enable_functions) { + $operator_characters = '[~=+-]'; + } else { + $operator_characters = '[+-]'; + } + + for ($ii = 0; $ii < $length; $ii++) { + $character = $query[$ii]; + + if ($mode == 'scan') { + if (preg_match('/^\s\z/u', $character)) { + continue; + } + + $mode = 'function'; + } + + if ($mode == 'function') { + $mode = 'operator'; + + if ($enable_functions) { + $found = false; + for ($jj = $ii; $jj < $length; $jj++) { + if (preg_match('/^[a-zA-Z]\z/u', $query[$jj])) { + continue; + } + if ($query[$jj] == ':') { + $found = $jj; + } + break; + } + + if ($found !== false) { + $function = array_slice($query, $ii, ($jj - $ii)); + $current_function = implode('', $function); + + if (!strlen($current_function)) { + $current_function = null; + } + + $ii = $jj; + continue; + } + } + } + + if ($mode == 'operator') { + if (preg_match('/^\s\z/u', $character)) { + continue; + } + + if (preg_match('/^'.$operator_characters.'\z/', $character)) { + $current_operator[] = $character; + continue; + } + + $mode = 'quote'; + } + + if ($mode == 'quote') { + if (preg_match('/^"\z/', $character)) { + $is_quoted = true; + $mode = 'token'; + continue; + } + + $mode = 'token'; + } + + if ($mode == 'token') { + $capture = false; + $was_quoted = $is_quoted; + if ($is_quoted) { + if (preg_match('/^"\z/', $character)) { + $capture = true; + $mode = 'scan'; + $is_quoted = false; + } + } else { + if (preg_match('/^\s\z/u', $character)) { + $capture = true; + $mode = 'scan'; + } + + if (preg_match('/^"\z/', $character)) { + $capture = true; + $mode = 'token'; + $is_quoted = true; + } + } + + if ($capture) { + $token = array( + 'operator' => $current_operator, + 'quoted' => $was_quoted, + 'value' => $current_token, + ); + + if ($enable_functions) { + $token['function'] = $current_function; + } + + $tokens[] = $token; + + $current_operator = array(); + $current_token = array(); + $current_function = null; + continue; + } else { + $current_token[] = $character; + } + } + } + + if ($is_quoted) { + throw new PhutilSearchQueryCompilerSyntaxException( + pht( + 'Query contains unmatched double quotes.')); + } + + if ($mode == 'operator') { + throw new PhutilSearchQueryCompilerSyntaxException( + pht( + 'Query contains operator ("%s") with no search term.', + implode('', $current_operator))); + } + + $token = array( + 'operator' => $current_operator, + 'quoted' => false, + 'value' => $current_token, + ); + + if ($enable_functions) { + $token['function'] = $current_function; + } + + $tokens[] = $token; + + $results = array(); + foreach ($tokens as $token) { + $value = implode('', $token['value']); + $operator_string = implode('', $token['operator']); + + if (!strlen($value)) { + continue; + } + + $is_quoted = $token['quoted']; + + switch ($operator_string) { + case '-': + $operator = self::OPERATOR_NOT; + break; + case '~': + $operator = self::OPERATOR_SUBSTRING; + break; + case '=': + $operator = self::OPERATOR_EXACT; + break; + case '+': + $operator = self::OPERATOR_AND; + break; + case '': + // See T12995. If this query term contains Chinese, Japanese or + // Korean characters, treat the term as a substring term by default. + // These languages do not separate words with spaces, so the term + // search mode is normally useless. + if ($enable_functions && !$is_quoted && phutil_utf8_is_cjk($value)) { + $operator = self::OPERATOR_SUBSTRING; + } else { + $operator = self::OPERATOR_AND; + } + break; + default: + throw new PhutilSearchQueryCompilerSyntaxException( + pht( + 'Query has an invalid sequence of operators ("%s").', + $operator_string)); + } + + $result = array( + 'operator' => $operator, + 'quoted' => $is_quoted, + 'value' => $value, + ); + + if ($enable_functions) { + $result['function'] = $token['function']; + } + + $results[] = $result; + } + + return $results; + } + + private function renderToken( + PhutilSearchQueryToken $token, + PhutilSearchStemmer $stemmer = null) { + $value = $token->getValue(); + + if ($stemmer) { + $value = $stemmer->stemToken($value); + } + + $value = $this->quoteToken($value); + $operator = $token->getOperator(); + $prefix = $this->getOperatorPrefix($operator); + + $value = $prefix.$value; + + return $value; + } + + private function getOperatorPrefix($operator) { + $operators = $this->operators; + + switch ($operator) { + case self::OPERATOR_AND: + $prefix = $operators[0]; + break; + case self::OPERATOR_NOT: + $prefix = $operators[2]; + break; + default: + throw new PhutilSearchQueryCompilerSyntaxException( + pht( + 'Unsupported operator prefix "%s".', + $operator)); + } + + if ($prefix == ' ') { + $prefix = null; + } + + return $prefix; + } + + private function quoteToken($value) { + $operators = $this->operators; + + $open_quote = $this->operators[10]; + $close_quote = $this->operators[11]; + + return $open_quote.$value.$close_quote; + } + +} diff --git a/src/applications/search/compiler/PhutilSearchQueryCompilerSyntaxException.php b/src/applications/search/compiler/PhutilSearchQueryCompilerSyntaxException.php new file mode 100644 index 0000000000..b09d36d04f --- /dev/null +++ b/src/applications/search/compiler/PhutilSearchQueryCompilerSyntaxException.php @@ -0,0 +1,4 @@ +isQuoted = $dictionary['quoted']; + $token->operator = $dictionary['operator']; + $token->value = $dictionary['value']; + $token->function = idx($dictionary, 'function'); + + return $token; + } + + public function isQuoted() { + return $this->isQuoted; + } + + public function getValue() { + return $this->value; + } + + public function getOperator() { + return $this->operator; + } + + public function getFunction() { + return $this->function; + } + +} diff --git a/src/applications/search/compiler/PhutilSearchStemmer.php b/src/applications/search/compiler/PhutilSearchStemmer.php new file mode 100644 index 0000000000..3255336ff0 --- /dev/null +++ b/src/applications/search/compiler/PhutilSearchStemmer.php @@ -0,0 +1,74 @@ +normalizeToken($token); + return $this->applyStemmer($token); + } + + public function stemCorpus($corpus) { + $corpus = $this->normalizeCorpus($corpus); + $tokens = preg_split('/[^a-zA-Z0-9\x7F-\xFF._]+/', $corpus); + + $words = array(); + foreach ($tokens as $key => $token) { + $token = trim($token, '._'); + + if (strlen($token) < 3) { + continue; + } + + $words[$token] = $token; + } + + $stems = array(); + foreach ($words as $word) { + $stems[] = $this->applyStemmer($word); + } + + return implode(' ', $stems); + } + + private function normalizeToken($token) { + return phutil_utf8_strtolower($token); + } + + private function normalizeCorpus($corpus) { + return phutil_utf8_strtolower($corpus); + } + + /** + * @phutil-external-symbol class Porter + */ + private function applyStemmer($normalized_token) { + // If the token has internal punctuation, handle it literally. This + // deals with things like domain names, Conduit API methods, and other + // sorts of informal tokens. + if (preg_match('/[._]/', $normalized_token)) { + return $normalized_token; + } + + static $loaded; + + if ($loaded === null) { + $root = dirname(phutil_get_library_root('phabricator')); + require_once $root.'/externals/porter-stemmer/src/Porter.php'; + $loaded = true; + } + + + $stem = Porter::stem($normalized_token); + + // If the stem is too short, it won't be a candidate for indexing. These + // tokens are also likely to be acronyms (like "DNS") rather than real + // English words. + if (strlen($stem) < 3) { + return $normalized_token; + } + + return $stem; + } + +} diff --git a/src/applications/search/compiler/__tests__/PhutilSearchQueryCompilerTestCase.php b/src/applications/search/compiler/__tests__/PhutilSearchQueryCompilerTestCase.php new file mode 100644 index 0000000000..647386f52c --- /dev/null +++ b/src/applications/search/compiler/__tests__/PhutilSearchQueryCompilerTestCase.php @@ -0,0 +1,220 @@ + null, + 'cat dog' => '+"cat" +"dog"', + 'cat -dog' => '+"cat" -"dog"', + 'cat-dog' => '+"cat-dog"', + + // If there are spaces after an operator, the operator applies to the + // next search term. + 'cat - dog' => '+"cat" -"dog"', + + // Double quotes serve as delimiters even if there is no whitespace + // between terms. + '"cat"dog' => '+"cat" +"dog"', + + // This query is too long. + str_repeat('x', 2048) => false, + + // Multiple operators are not permitted. + '++cat' => false, + '+-cat' => false, + '--cat' => false, + + // Stray operators are not permitted. + '+' => false, + 'cat +' => false, + + // Double quotes must be paired. + '"' => false, + 'cat "' => false, + '"cat' => false, + 'A"' => false, + 'A"B"' => '+"A" +"B"', + ); + + $this->assertCompileQueries($tests); + + // Test that we compile queries correctly if the operators have been + // swapped to use "AND" by default. + $operator_tests = array( + 'cat dog' => '"cat" "dog"', + 'cat -dog' => '"cat" -"dog"', + ); + $this->assertCompileQueries($operator_tests, ' |-><()~*:""&\''); + + + // Test that we compile queries correctly if the quote operators have + // been swapped to differ. + $quote_tests = array( + 'cat dog' => '+[cat] +[dog]', + 'cat -dog' => '+[cat] -[dog]', + ); + $this->assertCompileQueries($quote_tests, '+ -><()~*:[]&|'); + + } + + public function testCompileQueriesWithStemming() { + $stemming_tests = array( + 'cat dog' => array( + null, + '+"cat" +"dog"', + ), + 'cats dogs' => array( + null, + '+"cat" +"dog"', + ), + 'cats "dogs"' => array( + '+"dogs"', + '+"cat"', + ), + '"blessed blade" of the windseeker' => array( + '+"blessed blade"', + '+"of" +"the" +"windseek"', + ), + 'mailing users for mentions on tasks' => array( + null, + '+"mail" +"user" +"for" +"mention" +"on" +"task"', + ), + ); + + $stemmer = new PhutilSearchStemmer(); + $this->assertCompileQueries($stemming_tests, null, $stemmer); + } + + public function testCompileQueriesWithFunctions() { + $op_and = PhutilSearchQueryCompiler::OPERATOR_AND; + $op_sub = PhutilSearchQueryCompiler::OPERATOR_SUBSTRING; + $op_exact = PhutilSearchQueryCompiler::OPERATOR_EXACT; + + $mao = "\xE7\x8C\xAB"; + + $function_tests = array( + 'cat' => array( + array(null, $op_and, 'cat'), + ), + ':cat' => array( + array(null, $op_and, 'cat'), + ), + 'title:cat' => array( + array('title', $op_and, 'cat'), + ), + 'title:cat:dog' => array( + array('title', $op_and, 'cat:dog'), + ), + 'title:~cat' => array( + array('title', $op_sub, 'cat'), + ), + 'cat title:="Meow Meow"' => array( + array(null, $op_and, 'cat'), + array('title', $op_exact, 'Meow Meow'), + ), + 'title:cat title:dog' => array( + array('title', $op_and, 'cat'), + array('title', $op_and, 'dog'), + ), + '~"core and seven years ag"' => array( + array(null, $op_sub, 'core and seven years ag'), + ), + $mao => array( + array(null, $op_sub, $mao), + ), + '+'.$mao => array( + array(null, $op_and, $mao), + ), + '~'.$mao => array( + array(null, $op_sub, $mao), + ), + '"'.$mao.'"' => array( + array(null, $op_and, $mao), + ), + ); + + $this->assertCompileFunctionQueries($function_tests); + } + + private function assertCompileQueries( + array $tests, + $operators = null, + PhutilSearchStemmer $stemmer = null) { + foreach ($tests as $input => $expect) { + $caught = null; + + $query = null; + $literal_query = null; + $stemmed_query = null; + + try { + $compiler = new PhutilSearchQueryCompiler(); + + if ($operators !== null) { + $compiler->setOperators($operators); + } + + if ($stemmer !== null) { + $compiler->setStemmer($stemmer); + } + + $tokens = $compiler->newTokens($input); + + if ($stemmer) { + $literal_query = $compiler->compileLiteralQuery($tokens); + $stemmed_query = $compiler->compileStemmedQuery($tokens); + } else { + $query = $compiler->compileQuery($tokens); + } + } catch (PhutilSearchQueryCompilerSyntaxException $ex) { + $caught = $ex; + } + + if ($caught !== null) { + $query = false; + $literal_query = false; + $stemmed_query = false; + } + + if (!$stemmer) { + $this->assertEqual( + $expect, + $query, + pht('Compilation of query: %s', $input)); + } else { + $this->assertEqual( + $expect, + ($literal_query === false) + ? false + : array($literal_query, $stemmed_query), + pht('Stemmed compilation of query: %s', $input)); + } + } + } + + private function assertCompileFunctionQueries(array $tests) { + foreach ($tests as $input => $expect) { + $compiler = id(new PhutilSearchQueryCompiler()) + ->setEnableFunctions(true); + + $tokens = $compiler->newTokens($input); + + $result = array(); + foreach ($tokens as $token) { + $result[] = array( + $token->getFunction(), + $token->getOperator(), + $token->getValue(), + ); + } + + $this->assertEqual( + $expect, + $result, + pht('Function compilation of query: %s', $input)); + } + } + +} diff --git a/src/applications/search/compiler/__tests__/PhutilSearchStemmerTestCase.php b/src/applications/search/compiler/__tests__/PhutilSearchStemmerTestCase.php new file mode 100644 index 0000000000..34154d9ca3 --- /dev/null +++ b/src/applications/search/compiler/__tests__/PhutilSearchStemmerTestCase.php @@ -0,0 +1,85 @@ + 'token', + 'panels' => 'panel', + + 'renames' => 'renam', + 'rename' => 'renam', + + 'components' => 'compon', + 'component' => 'compon', + + 'implementation' => 'implement', + 'implements' => 'implement', + 'implementing' => 'implement', + 'implementer' => 'implement', + + 'deleting' => 'delet', + 'deletion' => 'delet', + 'delete' => 'delet', + + 'erratically' => 'errat', + 'erratic' => 'errat', + + // Stems should be normalized. + 'DOG' => 'dog', + + // If stemming would bring a token under 3 characters, it should not + // be stemmed. + 'dns' => 'dns', + 'nis' => 'nis', + + // Complex tokens with internal punctuation should be left untouched; + // these are usually things like domain names, API calls, informal tags, + // etc. + 'apples' => 'appl', + 'bananas' => 'banana', + 'apples_bananas' => 'apples_bananas', + 'apples_bananas.apples_bananas' => 'apples_bananas.apples_bananas', + ); + + $stemmer = new PhutilSearchStemmer(); + foreach ($tests as $input => $expect) { + $stem = $stemmer->stemToken($input); + $this->assertEqual( + $expect, + $stem, + pht('Token stem of "%s".', $input)); + } + } + + public function testStemDocuments() { + $tests = array( + 'The wild boar meandered erratically.' => + 'the wild boar meander errat', + 'Fool me onc, shame on you. Fool me twice, shame on me.' => + 'fool onc shame you twice', + 'Fireball is a seventh-level spell which deals 2d16 points of damage '. + 'in a 1-meter radius around a target.' => + 'firebal seventh level spell which deal 2d16 point damag meter '. + 'radiu around target', + 'apples-bananas' => 'appl banana', + 'apples_bananas' => 'apples_bananas', + 'apples.bananas' => 'apples.bananas', + 'oddly-proportioned' => 'oddli proport', + ); + + $stemmer = new PhutilSearchStemmer(); + foreach ($tests as $input => $expect) { + $stem = $stemmer->stemCorpus($input); + $this->assertEqual( + $expect, + $stem, + pht('Corpus stem of: %s', $input)); + } + } + + +} diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index c17ac1ec72..422605f761 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -5152,12 +5152,14 @@ abstract class PhabricatorApplicationTransactionEditor 'an MFA check.')); } - id(new PhabricatorAuthSessionEngine()) + $token = id(new PhabricatorAuthSessionEngine()) ->setWorkflowKey($workflow_key) ->requireHighSecurityToken($actor, $request, $cancel_uri); - foreach ($xactions as $xaction) { - $xaction->setIsMFATransaction(true); + if (!$token->getIsUnchallengedToken()) { + foreach ($xactions as $xaction) { + $xaction->setIsMFATransaction(true); + } } } diff --git a/support/startup/PhabricatorStartup.php b/support/startup/PhabricatorStartup.php index 4c577ca20c..c166162310 100644 --- a/support/startup/PhabricatorStartup.php +++ b/support/startup/PhabricatorStartup.php @@ -523,7 +523,7 @@ final class PhabricatorStartup { "'{$required_version}'."); } - if (get_magic_quotes_gpc()) { + if (@get_magic_quotes_gpc()) { self::didFatal( "Your server is configured with PHP 'magic_quotes_gpc' enabled. This ". "feature is 'highly discouraged' by PHP's developers and you must ". diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css index 3354be9663..6e1c4b3627 100644 --- a/webroot/rsrc/css/core/remarkup.css +++ b/webroot/rsrc/css/core/remarkup.css @@ -155,7 +155,6 @@ .phabricator-remarkup .remarkup-list-with-checkmarks .remarkup-checked-item { color: {$lightgreytext}; - text-decoration: line-through; } .phabricator-remarkup ul.remarkup-list ol.remarkup-list, diff --git a/webroot/rsrc/css/phui/phui-form.css b/webroot/rsrc/css/phui/phui-form.css index 1c587b5d3d..bb92df78ba 100644 --- a/webroot/rsrc/css/phui/phui-form.css +++ b/webroot/rsrc/css/phui/phui-form.css @@ -152,7 +152,7 @@ textarea[disabled], } .aphront-space-select-control-knob { - margin: 0 8px 0 0; + margin: 0 8px 4px 0; float: left; } @@ -162,7 +162,6 @@ textarea[disabled], } .device .aphront-space-select-control-knob { - margin-bottom: 8px; float: none; } diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css index e621d38134..7131db5640 100644 --- a/webroot/rsrc/css/phui/phui-header-view.css +++ b/webroot/rsrc/css/phui/phui-header-view.css @@ -203,6 +203,7 @@ body .phui-header-shell.phui-bleed-header margin-right: 8px; -webkit-font-smoothing: auto; border-color: transparent; + line-height: 28px; } diff --git a/webroot/rsrc/css/phui/phui-two-column-view.css b/webroot/rsrc/css/phui/phui-two-column-view.css index f9f86d950a..3e58f58173 100644 --- a/webroot/rsrc/css/phui/phui-two-column-view.css +++ b/webroot/rsrc/css/phui/phui-two-column-view.css @@ -50,7 +50,7 @@ } .phui-two-column-header .phui-header-subheader { - margin-top: 12px; + margin-top: 8px; } .phui-two-column-subheader {