1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-02-24 12:39:04 +01:00

(stable) Promote 2021 Week 8

This commit is contained in:
epriestley 2021-02-19 11:03:27 -08:00
commit 05745dfd02
50 changed files with 1945 additions and 592 deletions

View file

@ -9,8 +9,8 @@ return array(
'names' => array( 'names' => array(
'conpherence.pkg.css' => '0e3cf785', 'conpherence.pkg.css' => '0e3cf785',
'conpherence.pkg.js' => '020aebcf', 'conpherence.pkg.js' => '020aebcf',
'core.pkg.css' => '937616c0', 'core.pkg.css' => '0ae696de',
'core.pkg.js' => 'adc34883', 'core.pkg.js' => '079198f6',
'dark-console.pkg.js' => '187792c2', 'dark-console.pkg.js' => '187792c2',
'differential.pkg.css' => '5c459f92', 'differential.pkg.css' => '5c459f92',
'differential.pkg.js' => '5080baf4', 'differential.pkg.js' => '5080baf4',
@ -101,7 +101,7 @@ return array(
'rsrc/css/application/policy/policy-transaction-detail.css' => 'c02b8384', 'rsrc/css/application/policy/policy-transaction-detail.css' => 'c02b8384',
'rsrc/css/application/policy/policy.css' => 'ceb56a08', 'rsrc/css/application/policy/policy.css' => 'ceb56a08',
'rsrc/css/application/ponder/ponder-view.css' => '05a09d0a', 'rsrc/css/application/ponder/ponder-view.css' => '05a09d0a',
'rsrc/css/application/project/project-card-view.css' => '4e7371cd', 'rsrc/css/application/project/project-card-view.css' => 'a9f2c2dd',
'rsrc/css/application/project/project-triggers.css' => 'cd9c8bb9', 'rsrc/css/application/project/project-triggers.css' => 'cd9c8bb9',
'rsrc/css/application/project/project-view.css' => '567858b3', 'rsrc/css/application/project/project-view.css' => '567858b3',
'rsrc/css/application/releeph/releeph-core.css' => 'f81ff2db', 'rsrc/css/application/releeph/releeph-core.css' => 'f81ff2db',
@ -114,7 +114,7 @@ return array(
'rsrc/css/application/tokens/tokens.css' => 'ce5a50bd', 'rsrc/css/application/tokens/tokens.css' => 'ce5a50bd',
'rsrc/css/application/uiexample/example.css' => 'b4795059', 'rsrc/css/application/uiexample/example.css' => 'b4795059',
'rsrc/css/core/core.css' => 'b3ebd90d', 'rsrc/css/core/core.css' => 'b3ebd90d',
'rsrc/css/core/remarkup.css' => '24d48a73', 'rsrc/css/core/remarkup.css' => '5baa3bd9',
'rsrc/css/core/syntax.css' => '548567f6', 'rsrc/css/core/syntax.css' => '548567f6',
'rsrc/css/core/z-index.css' => 'ac3bfcd4', 'rsrc/css/core/z-index.css' => 'ac3bfcd4',
'rsrc/css/diviner/diviner-shared.css' => '4bd263b0', 'rsrc/css/diviner/diviner-shared.css' => '4bd263b0',
@ -151,7 +151,7 @@ return array(
'rsrc/css/phui/phui-comment-form.css' => '68a2d99a', 'rsrc/css/phui/phui-comment-form.css' => '68a2d99a',
'rsrc/css/phui/phui-comment-panel.css' => 'ec4e31c0', 'rsrc/css/phui/phui-comment-panel.css' => 'ec4e31c0',
'rsrc/css/phui/phui-crumbs-view.css' => '614f43cf', 'rsrc/css/phui/phui-crumbs-view.css' => '614f43cf',
'rsrc/css/phui/phui-curtain-object-ref-view.css' => '12404744', 'rsrc/css/phui/phui-curtain-object-ref-view.css' => '5f752bdb',
'rsrc/css/phui/phui-curtain-view.css' => '68c5efb6', 'rsrc/css/phui/phui-curtain-view.css' => '68c5efb6',
'rsrc/css/phui/phui-document-pro.css' => 'b9613a10', 'rsrc/css/phui/phui-document-pro.css' => 'b9613a10',
'rsrc/css/phui/phui-document-summary.css' => 'b068eed1', 'rsrc/css/phui/phui-document-summary.css' => 'b068eed1',
@ -180,8 +180,8 @@ return array(
'rsrc/css/phui/phui-remarkup-preview.css' => '91767007', 'rsrc/css/phui/phui-remarkup-preview.css' => '91767007',
'rsrc/css/phui/phui-segment-bar-view.css' => '5166b370', 'rsrc/css/phui/phui-segment-bar-view.css' => '5166b370',
'rsrc/css/phui/phui-spacing.css' => 'b05cadc3', 'rsrc/css/phui/phui-spacing.css' => 'b05cadc3',
'rsrc/css/phui/phui-status.css' => 'e5ff8be0', 'rsrc/css/phui/phui-status.css' => '293b5dad',
'rsrc/css/phui/phui-tag-view.css' => '8519160a', 'rsrc/css/phui/phui-tag-view.css' => 'fb811341',
'rsrc/css/phui/phui-timeline-view.css' => '2d32d7a9', 'rsrc/css/phui/phui-timeline-view.css' => '2d32d7a9',
'rsrc/css/phui/phui-two-column-view.css' => 'f96d319f', 'rsrc/css/phui/phui-two-column-view.css' => 'f96d319f',
'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308',
@ -460,7 +460,8 @@ return array(
'rsrc/js/core/DraggableList.js' => '0169e425', 'rsrc/js/core/DraggableList.js' => '0169e425',
'rsrc/js/core/Favicon.js' => '7930776a', 'rsrc/js/core/Favicon.js' => '7930776a',
'rsrc/js/core/FileUpload.js' => 'ab85e184', 'rsrc/js/core/FileUpload.js' => 'ab85e184',
'rsrc/js/core/Hovercard.js' => '074f0783', 'rsrc/js/core/Hovercard.js' => '6199f752',
'rsrc/js/core/HovercardList.js' => 'de4b4919',
'rsrc/js/core/KeyboardShortcut.js' => '1a844c06', 'rsrc/js/core/KeyboardShortcut.js' => '1a844c06',
'rsrc/js/core/KeyboardShortcutManager.js' => '81debc48', 'rsrc/js/core/KeyboardShortcutManager.js' => '81debc48',
'rsrc/js/core/MultirowRowManager.js' => '5b54c823', 'rsrc/js/core/MultirowRowManager.js' => '5b54c823',
@ -485,7 +486,7 @@ return array(
'rsrc/js/core/behavior-global-drag-and-drop.js' => '1cab0e9a', 'rsrc/js/core/behavior-global-drag-and-drop.js' => '1cab0e9a',
'rsrc/js/core/behavior-high-security-warning.js' => 'dae2d55b', 'rsrc/js/core/behavior-high-security-warning.js' => 'dae2d55b',
'rsrc/js/core/behavior-history-install.js' => '6a1583a8', 'rsrc/js/core/behavior-history-install.js' => '6a1583a8',
'rsrc/js/core/behavior-hovercard.js' => '6c379000', 'rsrc/js/core/behavior-hovercard.js' => '183738e6',
'rsrc/js/core/behavior-keyboard-pager.js' => '1325b731', 'rsrc/js/core/behavior-keyboard-pager.js' => '1325b731',
'rsrc/js/core/behavior-keyboard-shortcuts.js' => '42c44e8b', 'rsrc/js/core/behavior-keyboard-shortcuts.js' => '42c44e8b',
'rsrc/js/core/behavior-lightbox-attachments.js' => 'c7e748bf', 'rsrc/js/core/behavior-lightbox-attachments.js' => 'c7e748bf',
@ -670,7 +671,7 @@ return array(
'javelin-behavior-pholio-mock-view' => '5aa1544e', 'javelin-behavior-pholio-mock-view' => '5aa1544e',
'javelin-behavior-phui-dropdown-menu' => '5cf0501a', 'javelin-behavior-phui-dropdown-menu' => '5cf0501a',
'javelin-behavior-phui-file-upload' => 'e150bd50', 'javelin-behavior-phui-file-upload' => 'e150bd50',
'javelin-behavior-phui-hovercards' => '6c379000', 'javelin-behavior-phui-hovercards' => '183738e6',
'javelin-behavior-phui-selectable-list' => 'b26a41e4', 'javelin-behavior-phui-selectable-list' => 'b26a41e4',
'javelin-behavior-phui-submenu' => 'b5e9bff9', 'javelin-behavior-phui-submenu' => 'b5e9bff9',
'javelin-behavior-phui-tab-group' => '242aa08b', 'javelin-behavior-phui-tab-group' => '242aa08b',
@ -806,7 +807,7 @@ return array(
'phabricator-object-selector-css' => 'ee77366f', 'phabricator-object-selector-css' => 'ee77366f',
'phabricator-phtize' => '2f1db1ed', 'phabricator-phtize' => '2f1db1ed',
'phabricator-prefab' => '5793d835', 'phabricator-prefab' => '5793d835',
'phabricator-remarkup-css' => '24d48a73', 'phabricator-remarkup-css' => '5baa3bd9',
'phabricator-search-results-css' => '9ea70ace', 'phabricator-search-results-css' => '9ea70ace',
'phabricator-shaped-request' => '995f5102', 'phabricator-shaped-request' => '995f5102',
'phabricator-slowvote-css' => '1694baed', 'phabricator-slowvote-css' => '1694baed',
@ -845,7 +846,7 @@ return array(
'phui-comment-form-css' => '68a2d99a', 'phui-comment-form-css' => '68a2d99a',
'phui-comment-panel-css' => 'ec4e31c0', 'phui-comment-panel-css' => 'ec4e31c0',
'phui-crumbs-view-css' => '614f43cf', 'phui-crumbs-view-css' => '614f43cf',
'phui-curtain-object-ref-view-css' => '12404744', 'phui-curtain-object-ref-view-css' => '5f752bdb',
'phui-curtain-view-css' => '68c5efb6', 'phui-curtain-view-css' => '68c5efb6',
'phui-document-summary-view-css' => 'b068eed1', 'phui-document-summary-view-css' => 'b068eed1',
'phui-document-view-css' => '52b748a5', 'phui-document-view-css' => '52b748a5',
@ -858,7 +859,8 @@ return array(
'phui-formation-view-css' => 'd2dec8ed', 'phui-formation-view-css' => 'd2dec8ed',
'phui-head-thing-view-css' => 'd7f293df', 'phui-head-thing-view-css' => 'd7f293df',
'phui-header-view-css' => '36c86a58', 'phui-header-view-css' => '36c86a58',
'phui-hovercard' => '074f0783', 'phui-hovercard' => '6199f752',
'phui-hovercard-list' => 'de4b4919',
'phui-hovercard-view-css' => '6ca90fa0', 'phui-hovercard-view-css' => '6ca90fa0',
'phui-icon-set-selector-css' => '7aa5f3ec', 'phui-icon-set-selector-css' => '7aa5f3ec',
'phui-icon-view-css' => '4cbc684a', 'phui-icon-view-css' => '4cbc684a',
@ -883,8 +885,8 @@ return array(
'phui-remarkup-preview-css' => '91767007', 'phui-remarkup-preview-css' => '91767007',
'phui-segment-bar-view-css' => '5166b370', 'phui-segment-bar-view-css' => '5166b370',
'phui-spacing-css' => 'b05cadc3', 'phui-spacing-css' => 'b05cadc3',
'phui-status-list-view-css' => 'e5ff8be0', 'phui-status-list-view-css' => '293b5dad',
'phui-tag-view-css' => '8519160a', 'phui-tag-view-css' => 'fb811341',
'phui-theme-css' => '35883b37', 'phui-theme-css' => '35883b37',
'phui-timeline-view-css' => '2d32d7a9', 'phui-timeline-view-css' => '2d32d7a9',
'phui-two-column-view-css' => 'f96d319f', 'phui-two-column-view-css' => 'f96d319f',
@ -906,7 +908,7 @@ return array(
'policy-edit-css' => '8794e2ed', 'policy-edit-css' => '8794e2ed',
'policy-transaction-detail-css' => 'c02b8384', 'policy-transaction-detail-css' => 'c02b8384',
'ponder-view-css' => '05a09d0a', 'ponder-view-css' => '05a09d0a',
'project-card-view-css' => '4e7371cd', 'project-card-view-css' => 'a9f2c2dd',
'project-triggers-css' => 'cd9c8bb9', 'project-triggers-css' => 'cd9c8bb9',
'project-view-css' => '567858b3', 'project-view-css' => '567858b3',
'releeph-core' => 'f81ff2db', 'releeph-core' => 'f81ff2db',
@ -986,13 +988,6 @@ return array(
'javelin-uri', 'javelin-uri',
'phabricator-notification', 'phabricator-notification',
), ),
'074f0783' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
'javelin-request',
'javelin-uri',
),
'0889b835' => array( '0889b835' => array(
'javelin-install', 'javelin-install',
'javelin-event', 'javelin-event',
@ -1044,6 +1039,14 @@ return array(
'javelin-stratcom', 'javelin-stratcom',
'javelin-util', 'javelin-util',
), ),
'183738e6' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'javelin-vector',
'phui-hovercard',
'phui-hovercard-list',
),
'1a844c06' => array( '1a844c06' => array(
'javelin-install', 'javelin-install',
'javelin-util', 'javelin-util',
@ -1517,6 +1520,13 @@ return array(
'60cd9241' => array( '60cd9241' => array(
'javelin-behavior', 'javelin-behavior',
), ),
'6199f752' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
'javelin-request',
'javelin-uri',
),
'6337cf26' => array( '6337cf26' => array(
'javelin-behavior', 'javelin-behavior',
'javelin-dom', 'javelin-dom',
@ -1557,13 +1567,6 @@ return array(
'javelin-workflow', 'javelin-workflow',
'javelin-magical-init', 'javelin-magical-init',
), ),
'6c379000' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'javelin-vector',
'phui-hovercard',
),
'6cfa0008' => array( '6cfa0008' => array(
'javelin-dom', 'javelin-dom',
'javelin-dynval', 'javelin-dynval',
@ -2144,6 +2147,14 @@ return array(
'javelin-uri', 'javelin-uri',
'phabricator-notification', 'phabricator-notification',
), ),
'de4b4919' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
'javelin-request',
'javelin-uri',
'phui-hovercard',
),
'e150bd50' => array( 'e150bd50' => array(
'javelin-behavior', 'javelin-behavior',
'javelin-stratcom', 'javelin-stratcom',
@ -2367,6 +2378,7 @@ return array(
'javelin-behavior-global-drag-and-drop', 'javelin-behavior-global-drag-and-drop',
'javelin-behavior-phabricator-reveal-content', 'javelin-behavior-phabricator-reveal-content',
'phui-hovercard', 'phui-hovercard',
'phui-hovercard-list',
'javelin-behavior-phui-hovercards', 'javelin-behavior-phui-hovercards',
'javelin-color', 'javelin-color',
'javelin-fx', 'javelin-fx',

View file

@ -60,6 +60,7 @@ return array(
'javelin-behavior-global-drag-and-drop', 'javelin-behavior-global-drag-and-drop',
'javelin-behavior-phabricator-reveal-content', 'javelin-behavior-phabricator-reveal-content',
'phui-hovercard', 'phui-hovercard',
'phui-hovercard-list',
'javelin-behavior-phui-hovercards', 'javelin-behavior-phui-hovercards',
'javelin-color', 'javelin-color',
'javelin-fx', 'javelin-fx',

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_differential.differential_changeset
ADD phid VARBINARY(64) NOT NULL;

View file

@ -0,0 +1,71 @@
<?php
$phid_type = DifferentialChangesetPHIDType::TYPECONST;
$changeset_table = new DifferentialChangeset();
$conn = $changeset_table->establishConnection('w');
$table_name = $changeset_table->getTableName();
$chunk_size = 4096;
$temporary_table = 'tmp_20210215_changeset_id_map';
queryfx(
$conn,
'CREATE TEMPORARY TABLE %T (
changeset_id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
changeset_phid VARBINARY(64) NOT NULL)',
$temporary_table);
$table_iterator = id(new LiskRawMigrationIterator($conn, $table_name))
->setPageSize($chunk_size);
$chunk_iterator = new PhutilChunkedIterator($table_iterator, $chunk_size);
foreach ($chunk_iterator as $chunk) {
$map = array();
foreach ($chunk as $changeset_row) {
$phid = $changeset_row['phid'];
if (strlen($phid)) {
continue;
}
$phid = PhabricatorPHID::generateNewPHID($phid_type);
$id = $changeset_row['id'];
$map[(int)$id] = $phid;
}
if (!$map) {
continue;
}
$sql = array();
foreach ($map as $changeset_id => $changeset_phid) {
$sql[] = qsprintf(
$conn,
'(%d, %s)',
$changeset_id,
$changeset_phid);
}
queryfx(
$conn,
'TRUNCATE TABLE %T',
$temporary_table);
queryfx(
$conn,
'INSERT INTO %T (changeset_id, changeset_phid) VALUES %LQ',
$temporary_table,
$sql);
queryfx(
$conn,
'UPDATE %T c JOIN %T x ON c.id = x.changeset_id
SET c.phid = x.changeset_phid',
$table_name,
$temporary_table);
}

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_search.search_indexversion
ADD indexVersion BINARY(12) NOT NULL;

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_search.search_indexversion
ADD indexEpoch INT UNSIGNED NOT NULL;

View file

@ -472,10 +472,12 @@ phutil_register_library_map(array(
'DifferentialChangesetOneUpMailRenderer' => 'applications/differential/render/DifferentialChangesetOneUpMailRenderer.php', 'DifferentialChangesetOneUpMailRenderer' => 'applications/differential/render/DifferentialChangesetOneUpMailRenderer.php',
'DifferentialChangesetOneUpRenderer' => 'applications/differential/render/DifferentialChangesetOneUpRenderer.php', 'DifferentialChangesetOneUpRenderer' => 'applications/differential/render/DifferentialChangesetOneUpRenderer.php',
'DifferentialChangesetOneUpTestRenderer' => 'applications/differential/render/DifferentialChangesetOneUpTestRenderer.php', 'DifferentialChangesetOneUpTestRenderer' => 'applications/differential/render/DifferentialChangesetOneUpTestRenderer.php',
'DifferentialChangesetPHIDType' => 'applications/differential/phid/DifferentialChangesetPHIDType.php',
'DifferentialChangesetParser' => 'applications/differential/parser/DifferentialChangesetParser.php', 'DifferentialChangesetParser' => 'applications/differential/parser/DifferentialChangesetParser.php',
'DifferentialChangesetParserTestCase' => 'applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php', 'DifferentialChangesetParserTestCase' => 'applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php',
'DifferentialChangesetQuery' => 'applications/differential/query/DifferentialChangesetQuery.php', 'DifferentialChangesetQuery' => 'applications/differential/query/DifferentialChangesetQuery.php',
'DifferentialChangesetRenderer' => 'applications/differential/render/DifferentialChangesetRenderer.php', 'DifferentialChangesetRenderer' => 'applications/differential/render/DifferentialChangesetRenderer.php',
'DifferentialChangesetSearchConduitAPIMethod' => 'applications/differential/conduit/DifferentialChangesetSearchConduitAPIMethod.php',
'DifferentialChangesetSearchEngine' => 'applications/differential/query/DifferentialChangesetSearchEngine.php', 'DifferentialChangesetSearchEngine' => 'applications/differential/query/DifferentialChangesetSearchEngine.php',
'DifferentialChangesetTestRenderer' => 'applications/differential/render/DifferentialChangesetTestRenderer.php', 'DifferentialChangesetTestRenderer' => 'applications/differential/render/DifferentialChangesetTestRenderer.php',
'DifferentialChangesetTwoUpRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpRenderer.php', 'DifferentialChangesetTwoUpRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpRenderer.php',
@ -4270,6 +4272,7 @@ phutil_register_library_map(array(
'PhabricatorPolicyExplainController' => 'applications/policy/controller/PhabricatorPolicyExplainController.php', 'PhabricatorPolicyExplainController' => 'applications/policy/controller/PhabricatorPolicyExplainController.php',
'PhabricatorPolicyFavoritesSetting' => 'applications/settings/setting/PhabricatorPolicyFavoritesSetting.php', 'PhabricatorPolicyFavoritesSetting' => 'applications/settings/setting/PhabricatorPolicyFavoritesSetting.php',
'PhabricatorPolicyFilter' => 'applications/policy/filter/PhabricatorPolicyFilter.php', 'PhabricatorPolicyFilter' => 'applications/policy/filter/PhabricatorPolicyFilter.php',
'PhabricatorPolicyFilterSet' => 'applications/policy/filter/PhabricatorPolicyFilterSet.php',
'PhabricatorPolicyInterface' => 'applications/policy/interface/PhabricatorPolicyInterface.php', 'PhabricatorPolicyInterface' => 'applications/policy/interface/PhabricatorPolicyInterface.php',
'PhabricatorPolicyManagementShowWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php', 'PhabricatorPolicyManagementShowWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php',
'PhabricatorPolicyManagementUnlockWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php', 'PhabricatorPolicyManagementUnlockWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php',
@ -6531,6 +6534,7 @@ phutil_register_library_map(array(
'DifferentialDAO', 'DifferentialDAO',
'PhabricatorPolicyInterface', 'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface', 'PhabricatorDestructibleInterface',
'PhabricatorConduitResultInterface',
), ),
'DifferentialChangesetDetailView' => 'AphrontView', 'DifferentialChangesetDetailView' => 'AphrontView',
'DifferentialChangesetEngine' => 'Phobject', 'DifferentialChangesetEngine' => 'Phobject',
@ -6540,10 +6544,12 @@ phutil_register_library_map(array(
'DifferentialChangesetOneUpMailRenderer' => 'DifferentialChangesetRenderer', 'DifferentialChangesetOneUpMailRenderer' => 'DifferentialChangesetRenderer',
'DifferentialChangesetOneUpRenderer' => 'DifferentialChangesetHTMLRenderer', 'DifferentialChangesetOneUpRenderer' => 'DifferentialChangesetHTMLRenderer',
'DifferentialChangesetOneUpTestRenderer' => 'DifferentialChangesetTestRenderer', 'DifferentialChangesetOneUpTestRenderer' => 'DifferentialChangesetTestRenderer',
'DifferentialChangesetPHIDType' => 'PhabricatorPHIDType',
'DifferentialChangesetParser' => 'Phobject', 'DifferentialChangesetParser' => 'Phobject',
'DifferentialChangesetParserTestCase' => 'PhabricatorTestCase', 'DifferentialChangesetParserTestCase' => 'PhabricatorTestCase',
'DifferentialChangesetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DifferentialChangesetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'DifferentialChangesetRenderer' => 'Phobject', 'DifferentialChangesetRenderer' => 'Phobject',
'DifferentialChangesetSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'DifferentialChangesetSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DifferentialChangesetSearchEngine' => 'PhabricatorApplicationSearchEngine',
'DifferentialChangesetTestRenderer' => 'DifferentialChangesetRenderer', 'DifferentialChangesetTestRenderer' => 'DifferentialChangesetRenderer',
'DifferentialChangesetTwoUpRenderer' => 'DifferentialChangesetHTMLRenderer', 'DifferentialChangesetTwoUpRenderer' => 'DifferentialChangesetHTMLRenderer',
@ -10920,6 +10926,7 @@ phutil_register_library_map(array(
'PhabricatorPolicyExplainController' => 'PhabricatorPolicyController', 'PhabricatorPolicyExplainController' => 'PhabricatorPolicyController',
'PhabricatorPolicyFavoritesSetting' => 'PhabricatorInternalSetting', 'PhabricatorPolicyFavoritesSetting' => 'PhabricatorInternalSetting',
'PhabricatorPolicyFilter' => 'Phobject', 'PhabricatorPolicyFilter' => 'Phobject',
'PhabricatorPolicyFilterSet' => 'Phobject',
'PhabricatorPolicyInterface' => 'PhabricatorPHIDInterface', 'PhabricatorPolicyInterface' => 'PhabricatorPHIDInterface',
'PhabricatorPolicyManagementShowWorkflow' => 'PhabricatorPolicyManagementWorkflow', 'PhabricatorPolicyManagementShowWorkflow' => 'PhabricatorPolicyManagementWorkflow',
'PhabricatorPolicyManagementUnlockWorkflow' => 'PhabricatorPolicyManagementWorkflow', 'PhabricatorPolicyManagementUnlockWorkflow' => 'PhabricatorPolicyManagementWorkflow',

View file

@ -224,6 +224,43 @@ final class AphrontRequest extends Phobject {
} }
/**
* @task data
*/
public function getJSONMap($name, $default = array()) {
if (!isset($this->requestData[$name])) {
return $default;
}
$raw_data = phutil_string_cast($this->requestData[$name]);
$raw_data = trim($raw_data);
if (!strlen($raw_data)) {
return $default;
}
if ($raw_data[0] !== '{') {
throw new Exception(
pht(
'Request parameter "%s" is not formatted properly. Expected a '.
'JSON object, but value does not start with "{".',
$name));
}
try {
$json_object = phutil_json_decode($raw_data);
} catch (PhutilJSONParserException $ex) {
throw new Exception(
pht(
'Request parameter "%s" is not formatted properly. Expected a '.
'JSON object, but encountered a syntax error: %s.',
$name,
$ex->getMessage()));
}
return $json_object;
}
/** /**
* @task data * @task data
*/ */

View file

@ -5,13 +5,6 @@
*/ */
final class PhutilFacebookAuthAdapter extends PhutilOAuthAuthAdapter { final class PhutilFacebookAuthAdapter extends PhutilOAuthAuthAdapter {
private $requireSecureBrowsing;
public function setRequireSecureBrowsing($require_secure_browsing) {
$this->requireSecureBrowsing = $require_secure_browsing;
return $this;
}
public function getAdapterType() { public function getAdapterType() {
return 'facebook'; return 'facebook';
} }
@ -61,10 +54,6 @@ final class PhutilFacebookAuthAdapter extends PhutilOAuthAuthAdapter {
return $this->getOAuthAccountData('name'); return $this->getOAuthAccountData('name');
} }
public function getAccountSecuritySettings() {
return $this->getOAuthAccountData('security_settings');
}
protected function getAuthenticateBaseURI() { protected function getAuthenticateBaseURI() {
return 'https://www.facebook.com/dialog/oauth'; return 'https://www.facebook.com/dialog/oauth';
} }
@ -79,7 +68,6 @@ final class PhutilFacebookAuthAdapter extends PhutilOAuthAuthAdapter {
'name', 'name',
'email', 'email',
'link', 'link',
'security_settings',
'picture', 'picture',
); );
@ -97,17 +85,6 @@ final class PhutilFacebookAuthAdapter extends PhutilOAuthAuthAdapter {
$ex); $ex);
} }
if ($this->requireSecureBrowsing) {
if (empty($data['security_settings']['secure_browsing']['enabled'])) {
throw new Exception(
pht(
'This Phabricator install requires you to enable Secure Browsing '.
'on your Facebook account in order to use it to log in to '.
'Phabricator. For more information, see %s',
'https://www.facebook.com/help/156201551113407/'));
}
}
return $data; return $data;
} }

View file

@ -3,14 +3,35 @@
final class PhabricatorFacebookAuthProvider final class PhabricatorFacebookAuthProvider
extends PhabricatorOAuth2AuthProvider { extends PhabricatorOAuth2AuthProvider {
const KEY_REQUIRE_SECURE = 'oauth:facebook:require-secure';
public function getProviderName() { public function getProviderName() {
return pht('Facebook'); return pht('Facebook');
} }
protected function getProviderConfigurationHelp() { protected function getProviderConfigurationHelp() {
$uri = PhabricatorEnv::getProductionURI($this->getLoginURI()); $uri = PhabricatorEnv::getProductionURI($this->getLoginURI());
$domain = id(new PhutilURI($uri))->getDomain();
$table = array(
'Client OAuth Login' => pht('No'),
'Web OAuth Login' => pht('Yes'),
'Enforce HTTPS' => pht('Yes'),
'Force Web OAuth Reauthentication' => pht('Yes (Optional)'),
'Embedded Browser OAuth Login' => pht('No'),
'Use Strict Mode for Redirect URIs' => pht('Yes'),
'Login from Devices' => pht('No'),
'Valid OAuth Redirect URIs' => '`'.(string)$uri.'`',
'App Domains' => '`'.$domain.'`',
);
$rows = array();
foreach ($table as $k => $v) {
$rows[] = sprintf('| %s | %s |', $k, $v);
$rows[] = sprintf('|----| |');
}
$rows = implode("\n", $rows);
return pht( return pht(
'To configure Facebook OAuth, create a new Facebook Application here:'. 'To configure Facebook OAuth, create a new Facebook Application here:'.
"\n\n". "\n\n".
@ -18,29 +39,15 @@ final class PhabricatorFacebookAuthProvider
"\n\n". "\n\n".
'You should use these settings in your application:'. 'You should use these settings in your application:'.
"\n\n". "\n\n".
" - **Site URL**: Set this to `%s`\n". "%s\n".
" - **Valid OAuth redirect URIs**: You should also set this to `%s`\n".
" - **Client OAuth Login**: Set this to **OFF**.\n".
" - **Embedded browser OAuth Login**: Set this to **OFF**.\n".
"\n\n". "\n\n".
"Some of these settings may be in the **Advanced** tab.\n\n".
"After creating your new application, copy the **App ID** and ". "After creating your new application, copy the **App ID** and ".
"**App Secret** to the fields above.", "**App Secret** to the fields above.",
(string)$uri, $rows);
(string)$uri);
}
public function getDefaultProviderConfig() {
return parent::getDefaultProviderConfig()
->setProperty(self::KEY_REQUIRE_SECURE, 1);
} }
protected function newOAuthAdapter() { protected function newOAuthAdapter() {
$require_secure = $this->getProviderConfig()->getProperty( return new PhutilFacebookAuthAdapter();
self::KEY_REQUIRE_SECURE);
return id(new PhutilFacebookAuthAdapter())
->setRequireSecureBrowsing($require_secure);
} }
protected function getLoginIcon() { protected function getLoginIcon() {
@ -55,71 +62,4 @@ final class PhabricatorFacebookAuthProvider
); );
} }
public function readFormValuesFromProvider() {
$require_secure = $this->getProviderConfig()->getProperty(
self::KEY_REQUIRE_SECURE);
return parent::readFormValuesFromProvider() + array(
self::KEY_REQUIRE_SECURE => $require_secure,
);
}
public function readFormValuesFromRequest(AphrontRequest $request) {
return parent::readFormValuesFromRequest($request) + array(
self::KEY_REQUIRE_SECURE => $request->getBool(self::KEY_REQUIRE_SECURE),
);
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
parent::extendEditForm($request, $form, $values, $issues);
$key_require = self::KEY_REQUIRE_SECURE;
$v_require = idx($values, $key_require);
$form
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
$key_require,
$v_require,
pht(
"%s ".
"Require users to enable 'secure browsing' on Facebook in order ".
"to use Facebook to authenticate with Phabricator. This ".
"improves security by preventing an attacker from capturing ".
"an insecure Facebook session and escalating it into a ".
"Phabricator session. Enabling it is recommended.",
phutil_tag('strong', array(), pht('Require Secure Browsing:')))));
}
public function renderConfigPropertyTransactionTitle(
PhabricatorAuthProviderConfigTransaction $xaction) {
$author_phid = $xaction->getAuthorPHID();
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$key = $xaction->getMetadataValue(
PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);
switch ($key) {
case self::KEY_REQUIRE_SECURE:
if ($new) {
return pht(
'%s turned "Require Secure Browsing" on.',
$xaction->renderHandleLink($author_phid));
} else {
return pht(
'%s turned "Require Secure Browsing" off.',
$xaction->renderHandleLink($author_phid));
}
}
return parent::renderConfigPropertyTransactionTitle($xaction);
}
} }

View file

@ -0,0 +1,18 @@
<?php
final class DifferentialChangesetSearchConduitAPIMethod
extends PhabricatorSearchEngineAPIMethod {
public function getAPIMethodName() {
return 'differential.changeset.search';
}
public function newSearchEngine() {
return new DifferentialChangesetSearchEngine();
}
public function getMethodSummary() {
return pht('Read information about changesets.');
}
}

View file

@ -72,22 +72,40 @@ final class DifferentialReviewersField
return array(); return array();
} }
$viewer = $this->getViewer();
PhabricatorPolicyFilterSet::loadHandleViewCapabilities(
$viewer,
$handles,
array($revision));
$all_resigned = true; $all_resigned = true;
$all_disabled = true; $all_disabled = true;
$any_reviewers = false; $any_reviewers = false;
$all_exiled = true;
foreach ($this->getValue() as $reviewer) { foreach ($this->getValue() as $reviewer) {
$reviewer_phid = $reviewer->getReviewerPHID(); $reviewer_phid = $reviewer->getReviewerPHID();
$handle = $handles[$reviewer_phid];
$any_reviewers = true; $any_reviewers = true;
if (!$handles[$reviewer_phid]->isDisabled()) { if (!$handle->isDisabled()) {
$all_disabled = false; $all_disabled = false;
} }
if (!$reviewer->isResigned()) { if (!$reviewer->isResigned()) {
$all_resigned = false; $all_resigned = false;
} }
if (!$handle->hasCapabilities()) {
$all_exiled = false;
} else {
if ($handle->hasViewCapability($revision)) {
$all_exiled = false;
}
}
} }
$warnings = array(); $warnings = array();
@ -101,6 +119,10 @@ final class DifferentialReviewersField
} else if ($all_resigned) { } else if ($all_resigned) {
$warnings[] = pht( $warnings[] = pht(
'This revision needs review, but all reviewers have resigned.'); 'This revision needs review, but all reviewers have resigned.');
} else if ($all_exiled) {
$warnings[] = pht(
'This revision needs review, but no reviewers have permission '.
'to view it.');
} }
return $warnings; return $warnings;

View file

@ -0,0 +1,41 @@
<?php
final class DifferentialChangesetPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'DCNG';
public function getTypeName() {
return pht('Differential Changeset');
}
public function newObject() {
return new DifferentialChangeset();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorDifferentialApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new DifferentialChangesetQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$changeset = $objects[$phid];
$id = $changeset->getID();
$handle->setName(pht('Changeset %d', $id));
}
}
}

View file

@ -4,6 +4,9 @@ final class DifferentialChangesetQuery
extends PhabricatorCursorPagedPolicyAwareQuery { extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids; private $ids;
private $phids;
private $diffPHIDs;
private $diffs; private $diffs;
private $needAttachToDiffs; private $needAttachToDiffs;
@ -14,12 +17,22 @@ final class DifferentialChangesetQuery
return $this; return $this;
} }
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withDiffs(array $diffs) { public function withDiffs(array $diffs) {
assert_instances_of($diffs, 'DifferentialDiff'); assert_instances_of($diffs, 'DifferentialDiff');
$this->diffs = $diffs; $this->diffs = $diffs;
return $this; return $this;
} }
public function withDiffPHIDs(array $phids) {
$this->diffPHIDs = $phids;
return $this;
}
public function needAttachToDiffs($attach) { public function needAttachToDiffs($attach) {
$this->needAttachToDiffs = $attach; $this->needAttachToDiffs = $attach;
return $this; return $this;
@ -134,6 +147,31 @@ final class DifferentialChangesetQuery
$this->ids); $this->ids);
} }
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->diffPHIDs !== null) {
$diff_ids = queryfx_all(
$conn,
'SELECT id FROM %R WHERE phid IN (%Ls)',
new DifferentialDiff(),
$this->diffPHIDs);
$diff_ids = ipull($diff_ids, 'id', null);
if (!$diff_ids) {
throw new PhabricatorEmptyQueryException();
}
$where[] = qsprintf(
$conn,
'diffID IN (%Ld)',
$diff_ids);
}
return $where; return $where;
} }

View file

@ -38,11 +38,23 @@ final class DifferentialChangesetSearchEngine
protected function buildQueryFromParameters(array $map) { protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery(); $query = $this->newQuery();
if ($map['diffPHIDs']) {
$query->withDiffPHIDs($map['diffPHIDs']);
}
return $query; return $query;
} }
protected function buildCustomSearchFields() { protected function buildCustomSearchFields() {
return array(); return array(
id(new PhabricatorPHIDsSearchField())
->setLabel(pht('Diffs'))
->setKey('diffPHIDs')
->setAliases(array('diff', 'diffs', 'diffPHID'))
->setDescription(
pht('Find changesets attached to a particular diff.')),
);
} }
protected function getURI($path) { protected function getURI($path) {

View file

@ -4,7 +4,8 @@ final class DifferentialChangeset
extends DifferentialDAO extends DifferentialDAO
implements implements
PhabricatorPolicyInterface, PhabricatorPolicyInterface,
PhabricatorDestructibleInterface { PhabricatorDestructibleInterface,
PhabricatorConduitResultInterface {
protected $diffID; protected $diffID;
protected $oldFile; protected $oldFile;
@ -45,6 +46,7 @@ final class DifferentialChangeset
protected function getConfiguration() { protected function getConfiguration() {
return array( return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array( self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON, 'metadata' => self::SERIALIZATION_JSON,
'oldProperties' => self::SERIALIZATION_JSON, 'oldProperties' => self::SERIALIZATION_JSON,
@ -75,6 +77,10 @@ final class DifferentialChangeset
) + parent::getConfiguration(); ) + parent::getConfiguration();
} }
public function getPHIDType() {
return DifferentialChangesetPHIDType::TYPECONST;
}
public function getAffectedLineCount() { public function getAffectedLineCount() {
return $this->getAddLines() + $this->getDelLines(); return $this->getAddLines() + $this->getDelLines();
} }
@ -730,5 +736,49 @@ final class DifferentialChangeset
$this->saveTransaction(); $this->saveTransaction();
} }
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('diffPHID')
->setType('phid')
->setDescription(pht('The diff the changeset is attached to.')),
);
}
public function getFieldValuesForConduit() {
$diff = $this->getDiff();
$repository = null;
if ($diff) {
$revision = $diff->getRevision();
if ($revision) {
$repository = $revision->getRepository();
}
}
$absolute_path = $this->getAbsoluteRepositoryPath($repository, $diff);
if (strlen($absolute_path)) {
$absolute_path = base64_encode($absolute_path);
} else {
$absolute_path = null;
}
$display_path = $this->getDisplayFilename();
return array(
'diffPHID' => $diff->getPHID(),
'path' => array(
'displayPath' => $display_path,
'absolutePath.base64' => $absolute_path,
),
);
}
public function getConduitSearchAttachments() {
return array();
}
} }

View file

@ -26,6 +26,8 @@ final class DifferentialReviewersView extends AphrontView {
public function render() { public function render() {
$viewer = $this->getUser(); $viewer = $this->getUser();
$reviewers = $this->reviewers; $reviewers = $this->reviewers;
$diff = $this->diff;
$handles = $this->handles;
$view = new PHUIStatusListView(); $view = new PHUIStatusListView();
@ -40,10 +42,15 @@ final class DifferentialReviewersView extends AphrontView {
} }
} }
PhabricatorPolicyFilterSet::loadHandleViewCapabilities(
$viewer,
$handles,
array($diff));
$reviewers = $head + $tail; $reviewers = $head + $tail;
foreach ($reviewers as $reviewer) { foreach ($reviewers as $reviewer) {
$phid = $reviewer->getReviewerPHID(); $phid = $reviewer->getReviewerPHID();
$handle = $this->handles[$phid]; $handle = $handles[$phid];
$action_phid = $reviewer->getLastActionDiffPHID(); $action_phid = $reviewer->getLastActionDiffPHID();
$is_current_action = $this->isCurrent($action_phid); $is_current_action = $this->isCurrent($action_phid);
@ -154,7 +161,10 @@ final class DifferentialReviewersView extends AphrontView {
} }
$item->setIcon($icon, $color, $label); $item->setIcon($icon, $color, $label);
$item->setTarget($handle->renderHovercardLink()); $item->setTarget(
$handle->renderHovercardLink(
null,
$diff->getPHID()));
if ($reviewer->isPackage()) { if ($reviewer->isPackage()) {
if (!$reviewer->getChangesets()) { if (!$reviewer->getChangesets()) {
@ -162,6 +172,15 @@ final class DifferentialReviewersView extends AphrontView {
} }
} }
if ($handle->hasCapabilities()) {
if (!$handle->hasViewCapability($diff)) {
$item
->setIcon('fa-eye-slash', 'red')
->setNote(pht('No View Permission'))
->setIsExiled(true);
}
}
$view->addItem($item); $view->addItem($item);
} }

View file

@ -7,6 +7,14 @@ final class HarbormasterBuildTargetQuery
private $phids; private $phids;
private $buildPHIDs; private $buildPHIDs;
private $buildGenerations; private $buildGenerations;
private $dateCreatedMin;
private $dateCreatedMax;
private $dateStartedMin;
private $dateStartedMax;
private $dateCompletedMin;
private $dateCompletedMax;
private $statuses;
private $needBuildSteps; private $needBuildSteps;
public function withIDs(array $ids) { public function withIDs(array $ids) {
@ -29,6 +37,29 @@ final class HarbormasterBuildTargetQuery
return $this; return $this;
} }
public function withDateCreatedBetween($min, $max) {
$this->dateCreatedMin = $min;
$this->dateCreatedMax = $max;
return $this;
}
public function withDateStartedBetween($min, $max) {
$this->dateStartedMin = $min;
$this->dateStartedMax = $max;
return $this;
}
public function withDateCompletedBetween($min, $max) {
$this->dateCompletedMin = $min;
$this->dateCompletedMax = $max;
return $this;
}
public function withTargetStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function needBuildSteps($need_build_steps) { public function needBuildSteps($need_build_steps) {
$this->needBuildSteps = $need_build_steps; $this->needBuildSteps = $need_build_steps;
return $this; return $this;
@ -73,6 +104,55 @@ final class HarbormasterBuildTargetQuery
$this->buildGenerations); $this->buildGenerations);
} }
if ($this->dateCreatedMin !== null) {
$where[] = qsprintf(
$conn,
'dateCreated >= %d',
$this->dateCreatedMin);
}
if ($this->dateCreatedMax !== null) {
$where[] = qsprintf(
$conn,
'dateCreated <= %d',
$this->dateCreatedMax);
}
if ($this->dateStartedMin !== null) {
$where[] = qsprintf(
$conn,
'dateStarted >= %d',
$this->dateStartedMin);
}
if ($this->dateStartedMax !== null) {
$where[] = qsprintf(
$conn,
'dateStarted <= %d',
$this->dateStartedMax);
}
if ($this->dateCompletedMin !== null) {
$where[] = qsprintf(
$conn,
'dateCompleted >= %d',
$this->dateCompletedMin);
}
if ($this->dateCompletedMax !== null) {
$where[] = qsprintf(
$conn,
'dateCompleted <= %d',
$this->dateCompletedMax);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'targetStatus IN (%Ls)',
$this->statuses);
}
return $where; return $where;
} }

View file

@ -24,6 +24,42 @@ final class HarbormasterBuildTargetSearchEngine
->setDescription( ->setDescription(
pht('Search for targets of a given build.')) pht('Search for targets of a given build.'))
->setDatasource(new HarbormasterBuildPlanDatasource()), ->setDatasource(new HarbormasterBuildPlanDatasource()),
id(new PhabricatorSearchDateField())
->setLabel(pht('Created After'))
->setKey('createdStart')
->setDescription(
pht('Search for targets created on or after a particular date.')),
id(new PhabricatorSearchDateField())
->setLabel(pht('Created Before'))
->setKey('createdEnd')
->setDescription(
pht('Search for targets created on or before a particular date.')),
id(new PhabricatorSearchDateField())
->setLabel(pht('Started After'))
->setKey('startedStart')
->setDescription(
pht('Search for targets started on or after a particular date.')),
id(new PhabricatorSearchDateField())
->setLabel(pht('Started Before'))
->setKey('startedEnd')
->setDescription(
pht('Search for targets started on or before a particular date.')),
id(new PhabricatorSearchDateField())
->setLabel(pht('Completed After'))
->setKey('completedStart')
->setDescription(
pht('Search for targets completed on or after a particular date.')),
id(new PhabricatorSearchDateField())
->setLabel(pht('Completed Before'))
->setKey('completedEnd')
->setDescription(
pht('Search for targets completed on or before a particular date.')),
id(new PhabricatorSearchStringListField())
->setLabel(pht('Statuses'))
->setKey('statuses')
->setAliases(array('status'))
->setDescription(
pht('Search for targets with given statuses.')),
); );
} }
@ -34,6 +70,28 @@ final class HarbormasterBuildTargetSearchEngine
$query->withBuildPHIDs($map['buildPHIDs']); $query->withBuildPHIDs($map['buildPHIDs']);
} }
if ($map['createdStart'] !== null || $map['createdEnd'] !== null) {
$query->withDateCreatedBetween(
$map['createdStart'],
$map['createdEnd']);
}
if ($map['startedStart'] !== null || $map['startedEnd'] !== null) {
$query->withDateStartedBetween(
$map['startedStart'],
$map['startedEnd']);
}
if ($map['completedStart'] !== null || $map['completedEnd'] !== null) {
$query->withDateCompletedBetween(
$map['completedStart'],
$map['completedEnd']);
}
if ($map['statuses']) {
$query->withTargetStatuses($map['statuses']);
}
return $query; return $query;
} }

View file

@ -128,6 +128,11 @@ final class HarbormasterBuild extends HarbormasterDAO
'step.timestamp' => null, 'step.timestamp' => null,
'build.id' => null, 'build.id' => null,
'initiator.phid' => null, 'initiator.phid' => null,
'buildable.phid' => null,
'buildable.object.phid' => null,
'buildable.container.phid' => null,
'build.phid' => null,
); );
foreach ($this->getBuildParameters() as $key => $value) { foreach ($this->getBuildParameters() as $key => $value) {
@ -145,6 +150,11 @@ final class HarbormasterBuild extends HarbormasterDAO
$results['build.id'] = $this->getID(); $results['build.id'] = $this->getID();
$results['initiator.phid'] = $this->getInitiatorPHID(); $results['initiator.phid'] = $this->getInitiatorPHID();
$results['buildable.phid'] = $buildable->getPHID();
$results['buildable.object.phid'] = $object->getPHID();
$results['buildable.container.phid'] = $buildable->getContainerPHID();
$results['build.phid'] = $this->getPHID();
return $results; return $results;
} }
@ -161,6 +171,16 @@ final class HarbormasterBuild extends HarbormasterDAO
'initiator.phid' => pht( 'initiator.phid' => pht(
'The PHID of the user or Object that initiated the build, '. 'The PHID of the user or Object that initiated the build, '.
'if applicable.'), 'if applicable.'),
'buildable.phid' => pht(
'The object PHID of the Harbormaster Buildable being built.'),
'buildable.object.phid' => pht(
'The object PHID of the object (usually a diff or commit) '.
'being built.'),
'buildable.container.phid' => pht(
'The object PHID of the container (usually a revision or repository) '.
'for the object being built.'),
'build.phid' => pht(
'The object PHID of the Harbormaster Build being built.'),
); );
foreach ($objects as $object) { foreach ($objects as $object) {

View file

@ -119,6 +119,15 @@ final class HarbormasterBuildTarget
'key_build' => array( 'key_build' => array(
'columns' => array('buildPHID', 'buildStepPHID'), 'columns' => array('buildPHID', 'buildStepPHID'),
), ),
'key_started' => array(
'columns' => array('dateStarted'),
),
'key_completed' => array(
'columns' => array('dateCompleted'),
),
'key_created' => array(
'columns' => array('dateCreated'),
),
), ),
) + parent::getConfiguration(); ) + parent::getConfiguration();
} }

View file

@ -638,7 +638,9 @@ final class ManiphestTaskDetailController extends ManiphestController {
'href' => $commit->getURI(), 'href' => $commit->getURI(),
'sigil' => 'hovercard', 'sigil' => 'hovercard',
'meta' => array( 'meta' => array(
'hoverPHID' => $commit->getPHID(), 'hovercardSpec' => array(
'objectPHID' => $commit->getPHID(),
),
), ),
), ),
$commit->getSummary()); $commit->getSummary());
@ -705,7 +707,9 @@ final class ManiphestTaskDetailController extends ManiphestController {
'href' => $revision->getURI(), 'href' => $revision->getURI(),
'sigil' => 'hovercard', 'sigil' => 'hovercard',
'meta' => array( 'meta' => array(
'hoverPHID' => $revision->getPHID(), 'hovercardSpec' => array(
'objectPHID' => $revision->getPHID(),
),
), ),
), ),
$revision->getTitle()); $revision->getTitle());

View file

@ -47,12 +47,14 @@ final class PeopleHovercardEngineExtension
return; return;
} }
$is_exiled = $hovercard->getIsExiled();
$user_card = id(new PhabricatorUserCardView()) $user_card = id(new PhabricatorUserCardView())
->setProfile($user) ->setProfile($user)
->setViewer($viewer); ->setViewer($viewer)
->setIsExiled($is_exiled);
$hovercard->appendChild($user_card); $hovercard->appendChild($user_card);
} }
} }

View file

@ -87,24 +87,36 @@ final class PhabricatorMentionRemarkupRule extends PhutilRemarkupRule {
$engine->setTextMetadata($mentioned_key, $mentioned); $engine->setTextMetadata($mentioned_key, $mentioned);
$context_object = $engine->getConfig('contextObject'); $context_object = $engine->getConfig('contextObject');
$policy_object = null;
if ($context_object) {
if ($context_object instanceof PhabricatorPolicyInterface) {
$policy_object = $context_object;
}
}
if ($policy_object) {
$policy_set = new PhabricatorPolicyFilterSet();
foreach ($actual_users as $user) {
$policy_set->addCapability(
$user,
$policy_object,
PhabricatorPolicyCapability::CAN_VIEW);
}
}
foreach ($metadata as $username => $tokens) { foreach ($metadata as $username => $tokens) {
$exists = isset($actual_users[$username]); $exists = isset($actual_users[$username]);
$user_has_no_permission = false; $user_can_not_view = false;
if ($exists) { if ($exists) {
$user = $actual_users[$username]; $user = $actual_users[$username];
Javelin::initBehavior('phui-hovercards');
// Check if the user has view access to the object she was mentioned in // Check if the user has view access to the object she was mentioned in
if ($context_object if ($policy_object) {
&& $context_object instanceof PhabricatorPolicyInterface) { $user_can_not_view = !$policy_set->hasCapability(
if (!PhabricatorPolicyFilter::hasCapability(
$user, $user,
$context_object, $policy_object,
PhabricatorPolicyCapability::CAN_VIEW)) { PhabricatorPolicyCapability::CAN_VIEW);
// User mentioned has no permission to this object
$user_has_no_permission = true;
}
} }
$user_href = '/p/'.$user->getUserName().'/'; $user_href = '/p/'.$user->getUserName().'/';
@ -112,7 +124,7 @@ final class PhabricatorMentionRemarkupRule extends PhutilRemarkupRule {
if ($engine->isHTMLMailMode()) { if ($engine->isHTMLMailMode()) {
$user_href = PhabricatorEnv::getProductionURI($user_href); $user_href = PhabricatorEnv::getProductionURI($user_href);
if ($user_has_no_permission) { if ($user_can_not_view) {
$colors = ' $colors = '
border-color: #92969D; border-color: #92969D;
color: #92969D; color: #92969D;
@ -146,8 +158,13 @@ final class PhabricatorMentionRemarkupRule extends PhutilRemarkupRule {
->setName('@'.$user->getUserName()) ->setName('@'.$user->getUserName())
->setHref($user_href); ->setHref($user_href);
if ($user_has_no_permission) { if ($context_object) {
$tag->addClass('phabricator-remarkup-mention-nopermission'); $tag->setContextObject($context_object);
}
if ($user_can_not_view) {
$tag->setIcon('fa-eye-slash red');
$tag->setIsExiled(true);
} }
if ($user->getIsDisabled()) { if ($user->getIsDisabled()) {

View file

@ -5,6 +5,7 @@ final class PhabricatorUserCardView extends AphrontTagView {
private $profile; private $profile;
private $viewer; private $viewer;
private $tag; private $tag;
private $isExiled;
public function setProfile(PhabricatorUser $profile) { public function setProfile(PhabricatorUser $profile) {
$this->profile = $profile; $this->profile = $profile;
@ -42,6 +43,15 @@ final class PhabricatorUserCardView extends AphrontTagView {
); );
} }
public function setIsExiled($is_exiled) {
$this->isExiled = $is_exiled;
return $this;
}
public function getIsExiled() {
return $this->isExiled;
}
protected function getTagContent() { protected function getTagContent() {
$user = $this->profile; $user = $this->profile;
@ -108,6 +118,15 @@ final class PhabricatorUserCardView extends AphrontTagView {
} }
} }
if ($this->getIsExiled()) {
$body[] = $this->addItem(
'fa-eye-slash red',
pht('This user can not see this object.'),
array(
'project-card-item-exiled',
));
}
$classes[] = 'project-card-image'; $classes[] = 'project-card-image';
$image = phutil_tag( $image = phutil_tag(
'img', 'img',
@ -160,17 +179,26 @@ final class PhabricatorUserCardView extends AphrontTagView {
return $card; return $card;
} }
private function addItem($icon, $value) { private function addItem($icon, $value, $classes = array()) {
$classes[] = 'project-card-item';
$icon = id(new PHUIIconView()) $icon = id(new PHUIIconView())
->addClass('project-card-item-icon') ->addClass('project-card-item-icon')
->setIcon($icon); ->setIcon($icon);
$text = phutil_tag( $text = phutil_tag(
'span', 'span',
array( array(
'class' => 'project-card-item-text', 'class' => 'project-card-item-text',
), ),
$value); $value);
return phutil_tag_div('project-card-item', array($icon, $text));
return phutil_tag(
'div',
array(
'class' => implode(' ', $classes),
),
array($icon, $text));
} }
} }

View file

@ -32,6 +32,7 @@ final class PhabricatorObjectHandle
private $tokenIcon; private $tokenIcon;
private $commandLineObjectName; private $commandLineObjectName;
private $mailStampName; private $mailStampName;
private $capabilities = array();
public function setIcon($icon) { public function setIcon($icon) {
$this->icon = $icon; $this->icon = $icon;
@ -299,13 +300,21 @@ final class PhabricatorObjectHandle
return $this->renderLinkWithAttributes($name, array()); return $this->renderLinkWithAttributes($name, array());
} }
public function renderHovercardLink($name = null) { public function renderHovercardLink($name = null, $context_phid = null) {
Javelin::initBehavior('phui-hovercards'); Javelin::initBehavior('phui-hovercards');
$hovercard_spec = array(
'objectPHID' => $this->getPHID(),
);
if ($context_phid) {
$hovercard_spec['contextPHID'] = $context_phid;
}
$attributes = array( $attributes = array(
'sigil' => 'hovercard', 'sigil' => 'hovercard',
'meta' => array( 'meta' => array(
'hoverPHID' => $this->getPHID(), 'hovercardSpec' => $hovercard_spec,
), ),
); );
@ -388,6 +397,68 @@ final class PhabricatorObjectHandle
return idx($types, $this->getType()); return idx($types, $this->getType());
} }
public function hasCapabilities() {
return ($this->getType() === PhabricatorPeopleUserPHIDType::TYPECONST);
}
public function attachCapability(
PhabricatorPolicyInterface $object,
$capability,
$has_capability) {
if (!$this->hasCapabilities()) {
throw new Exception(
pht(
'Attempting to attach capability ("%s") for object ("%s") to '.
'handle, but this handle (of type "%s") can not have '.
'capabilities.',
$capability,
get_class($object),
$this->getType()));
}
$object_key = $this->getObjectCapabilityKey($object);
$this->capabilities[$object_key][$capability] = $has_capability;
return $this;
}
public function hasViewCapability(PhabricatorPolicyInterface $object) {
return $this->hasCapability($object, PhabricatorPolicyCapability::CAN_VIEW);
}
private function hasCapability(
PhabricatorPolicyInterface $object,
$capability) {
$object_key = $this->getObjectCapabilityKey($object);
if (!isset($this->capabilities[$object_key][$capability])) {
throw new Exception(
pht(
'Attempting to test capability "%s" for handle of type "%s", but '.
'this capability has not been attached.',
$capability,
$this->getType()));
}
return $this->capabilities[$object_key][$capability];
}
private function getObjectCapabilityKey(PhabricatorPolicyInterface $object) {
$object_phid = $object->getPHID();
if (!$object_phid) {
throw new Exception(
pht(
'Object (of class "%s") has no PHID, so handles can not interact '.
'with capabilities for it.',
get_class($object)));
}
return $object_phid;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */ /* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -0,0 +1,167 @@
<?php
final class PhabricatorPolicyFilterSet
extends Phobject {
private $users = array();
private $objects = array();
private $capabilities = array();
private $queue = array();
private $results = array();
public function addCapability(
PhabricatorUser $user,
PhabricatorPolicyInterface $object,
$capability) {
$user_key = $this->getUserKey($user);
$this->users[$user_key] = $user;
$object_key = $this->getObjectKey($object);
$this->objects[$object_key] = $object;
if (!isset($this->capabilities[$capability][$user_key][$object_key])) {
$this->capabilities[$capability][$user_key][$object_key] = true;
$this->queue[$capability][$user_key][$object_key] = true;
}
return $this;
}
public function hasCapability(
PhabricatorUser $user,
PhabricatorPolicyInterface $object,
$capability) {
$user_key = $this->getUserKey($user);
$this->users[$user_key] = $user;
$object_key = $this->getObjectKey($object);
$this->objects[$object_key] = $object;
if (!isset($this->capabilities[$capability][$user_key][$object_key])) {
throw new Exception(
pht(
'Capability "%s" for user "%s" on object "%s" is being resolved, '.
'but was never queued with "addCapability()".',
$capability,
$user_key,
$object_key));
}
if (!isset($this->results[$capability][$user_key][$object_key])) {
$this->resolveCapabilities();
}
return $this->results[$capability][$user_key][$object_key];
}
private function getUserKey(PhabricatorUser $user) {
return $user->getCacheFragment();
}
private function getObjectKey(PhabricatorPolicyInterface $object) {
$object_phid = $object->getPHID();
if (!$object_phid) {
throw new Exception(
pht(
'Unable to perform capability tests on an object (of class "%s") '.
'with no PHID.',
get_class($object)));
}
return $object_phid;
}
private function resolveCapabilities() {
// This class is primarily used to test if a list of users (like
// subscribers) can see a single object. It is not structured in a way
// that makes this particularly efficient, and performance would probably
// be improved if filtering supported this use case more narrowly.
foreach ($this->queue as $capability => $user_map) {
foreach ($user_map as $user_key => $object_map) {
$user = $this->users[$user_key];
$objects = array_select_keys($this->objects, array_keys($object_map));
$filter = id(new PhabricatorPolicyFilter())
->setViewer($user)
->requireCapabilities(array($capability));
$results = $filter->apply($objects);
foreach ($object_map as $object_key => $object) {
$has_capability = (bool)isset($results[$object_key]);
$this->results[$capability][$user_key][$object_key] = $has_capability;
}
}
}
$this->queue = array();
}
public static function loadHandleViewCapabilities(
$viewer,
$handles,
array $objects) {
$capabilities = array(
PhabricatorPolicyCapability::CAN_VIEW,
);
assert_instances_of($objects, 'PhabricatorPolicyInterface');
if (!$objects) {
return;
}
$viewer_map = array();
foreach ($handles as $handle_key => $handle) {
if (!$handle->hasCapabilities()) {
continue;
}
$viewer_map[$handle->getPHID()] = $handle_key;
}
if (!$viewer_map) {
return;
}
$users = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array_keys($viewer_map))
->execute();
$users = mpull($users, null, 'getPHID');
$filter_set = new self();
foreach ($users as $user_phid => $user) {
foreach ($objects as $object) {
foreach ($capabilities as $capability) {
$filter_set->addCapability($user, $object, $capability);
}
}
}
foreach ($users as $user_phid => $user) {
$handle_key = $viewer_map[$user_phid];
$handle = $handles[$handle_key];
foreach ($objects as $object) {
foreach ($capabilities as $capability) {
$has_capability = $filter_set->hasCapability(
$user,
$object,
$capability);
$handle->attachCapability(
$object,
$capability,
$has_capability);
}
}
}
}
}

View file

@ -9,7 +9,8 @@ final class PhabricatorSearchHovercardController
public function handleRequest(AphrontRequest $request) { public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer(); $viewer = $this->getViewer();
$phids = $request->getArr('phids');
$cards = $request->getJSONMap('cards');
// If object names are provided, look them up and pretend they were // If object names are provided, look them up and pretend they were
// passed as additional PHIDs. This is primarily useful for debugging, // passed as additional PHIDs. This is primarily useful for debugging,
@ -23,26 +24,54 @@ final class PhabricatorSearchHovercardController
->execute(); ->execute();
foreach ($named_objects as $object) { foreach ($named_objects as $object) {
$phids[] = $object->getPHID(); $cards[] = array(
'objectPHID' => $object->getPHID(),
);
}
}
$object_phids = array();
$handle_phids = array();
$context_phids = array();
foreach ($cards as $card) {
$object_phid = idx($card, 'objectPHID');
$handle_phids[] = $object_phid;
$object_phids[] = $object_phid;
$context_phid = idx($card, 'contextPHID');
if ($context_phid) {
$object_phids[] = $context_phid;
$context_phids[] = $context_phid;
} }
} }
$handles = id(new PhabricatorHandleQuery()) $handles = id(new PhabricatorHandleQuery())
->setViewer($viewer) ->setViewer($viewer)
->withPHIDs($phids) ->withPHIDs($handle_phids)
->execute(); ->execute();
$objects = id(new PhabricatorObjectQuery()) $objects = id(new PhabricatorObjectQuery())
->setViewer($viewer) ->setViewer($viewer)
->withPHIDs($phids) ->withPHIDs($object_phids)
->execute(); ->execute();
$objects = mpull($objects, null, 'getPHID'); $objects = mpull($objects, null, 'getPHID');
$context_objects = array_select_keys($objects, $context_phids);
if ($context_objects) {
PhabricatorPolicyFilterSet::loadHandleViewCapabilities(
$viewer,
$handles,
$context_objects);
}
$extensions = $extensions =
PhabricatorHovercardEngineExtension::getAllEnabledExtensions(); PhabricatorHovercardEngineExtension::getAllEnabledExtensions();
$extension_maps = array(); $extension_maps = array();
foreach ($extensions as $key => $extension) { foreach ($extensions as $extension_key => $extension) {
$extension->setViewer($viewer); $extension->setViewer($viewer);
$extension_phids = array(); $extension_phids = array();
@ -52,56 +81,73 @@ final class PhabricatorSearchHovercardController
} }
} }
$extension_maps[$key] = $extension_phids; $extension_maps[$extension_key] = $extension_phids;
} }
$extension_data = array(); $extension_data = array();
foreach ($extensions as $key => $extension) { foreach ($extensions as $extension_key => $extension) {
$extension_phids = $extension_maps[$key]; $extension_phids = $extension_maps[$extension_key];
if (!$extension_phids) { if (!$extension_phids) {
unset($extensions[$key]); unset($extensions[$extension_key]);
continue; continue;
} }
$extension_data[$key] = $extension->willRenderHovercards( $extension_data[$extension_key] = $extension->willRenderHovercards(
array_select_keys($objects, $extension_phids)); array_select_keys($objects, $extension_phids));
} }
$cards = array(); $results = array();
foreach ($phids as $phid) { foreach ($cards as $card_key => $card) {
$handle = $handles[$phid]; $object_phid = $card['objectPHID'];
$object = idx($objects, $phid);
$handle = $handles[$object_phid];
$object = idx($objects, $object_phid);
$context_phid = idx($card, 'contextPHID');
if ($context_phid) {
$context_object = idx($context_objects, $context_phid);
} else {
$context_object = null;
}
$hovercard = id(new PHUIHovercardView()) $hovercard = id(new PHUIHovercardView())
->setUser($viewer) ->setUser($viewer)
->setObjectHandle($handle); ->setObjectHandle($handle);
if ($object) { if ($context_object) {
$hovercard->setObject($object); if ($handle->hasCapabilities()) {
if (!$handle->hasViewCapability($context_object)) {
foreach ($extension_maps as $key => $extension_phids) { $hovercard->setIsExiled(true);
if (isset($extension_phids[$phid])) {
$extensions[$key]->renderHovercard(
$hovercard,
$handle,
$object,
$extension_data[$key]);
} }
} }
} }
$cards[$phid] = $hovercard; if ($object) {
$hovercard->setObject($object);
foreach ($extension_maps as $extension_key => $extension_phids) {
if (isset($extension_phids[$object_phid])) {
$extensions[$extension_key]->renderHovercard(
$hovercard,
$handle,
$object,
$extension_data[$extension_key]);
}
}
}
$results[$card_key] = $hovercard;
} }
if ($request->isAjax()) { if ($request->isAjax()) {
return id(new AphrontAjaxResponse())->setContent( return id(new AphrontAjaxResponse())->setContent(
array( array(
'cards' => $cards, 'cards' => $results,
)); ));
} }
foreach ($cards as $key => $hovercard) { foreach ($results as $result_key => $hovercard) {
$cards[$key] = phutil_tag('div', $results[$result_key] = phutil_tag('div',
array( array(
'class' => 'ml', 'class' => 'ml',
), ),
@ -109,7 +155,7 @@ final class PhabricatorSearchHovercardController
} }
return $this->newPage() return $this->newPage()
->appendChild($cards) ->appendChild($results)
->setShowFooter(false); ->setShowFooter(false);
} }

View file

@ -134,123 +134,297 @@ final class PhabricatorFerretFulltextEngineExtension
$ngram_engine = new PhabricatorSearchNgramEngine(); $ngram_engine = new PhabricatorSearchNgramEngine();
$ngrams = $ngram_engine->getTermNgramsFromString($ngrams_source); $ngrams = $ngram_engine->getTermNgramsFromString($ngrams_source);
$conn = $object->establishConnection('w');
if ($ngrams) {
$common = queryfx_all(
$conn,
'SELECT ngram FROM %T WHERE ngram IN (%Ls)',
$engine->getCommonNgramsTableName(),
$ngrams);
$common = ipull($common, 'ngram', 'ngram');
foreach ($ngrams as $key => $ngram) {
if (isset($common[$ngram])) {
unset($ngrams[$key]);
continue;
}
// NOTE: MySQL discards trailing whitespace in CHAR(X) columns.
$trimmed_ngram = rtrim($ngram, ' ');
if (isset($common[$trimmed_ngram])) {
unset($ngrams[$key]);
continue;
}
}
}
$object->openTransaction(); $object->openTransaction();
try { try {
$conn = $object->establishConnection('w'); // See T13587. If this document already exists in the index, we try to
$this->deleteOldDocument($engine, $object, $document); // update the existing rows to avoid leaving the ngrams table heavily
// fragmented.
queryfx( $old_document = queryfx_one(
$conn, $conn,
'INSERT INTO %T (objectPHID, isClosed, epochCreated, epochModified, 'SELECT id FROM %T WHERE objectPHID = %s',
authorPHID, ownerPHID) VALUES (%s, %d, %d, %d, %ns, %ns)',
$engine->getDocumentTableName(), $engine->getDocumentTableName(),
$object->getPHID(), $object->getPHID());
$is_closed, if ($old_document) {
$document->getDocumentCreated(), $old_document_id = (int)$old_document['id'];
$document->getDocumentModified(), } else {
$author_phid, $old_document_id = null;
$owner_phid); }
$document_id = $conn->getInsertID(); if ($old_document_id === null) {
foreach ($ferret_fields as $ferret_field) {
queryfx( queryfx(
$conn, $conn,
'INSERT INTO %T (documentID, fieldKey, rawCorpus, termCorpus, 'INSERT INTO %T (objectPHID, isClosed, epochCreated, epochModified,
normalCorpus) VALUES (%d, %s, %s, %s, %s)', authorPHID, ownerPHID) VALUES (%s, %d, %d, %d, %ns, %ns)',
$engine->getFieldTableName(), $engine->getDocumentTableName(),
$document_id, $object->getPHID(),
$ferret_field['fieldKey'], $is_closed,
$ferret_field['rawCorpus'], $document->getDocumentCreated(),
$ferret_field['termCorpus'], $document->getDocumentModified(),
$ferret_field['normalCorpus']); $author_phid,
} $owner_phid);
$document_id = $conn->getInsertID();
if ($ngrams) { $is_new = true;
$common = queryfx_all( } else {
$document_id = $old_document_id;
queryfx(
$conn, $conn,
'SELECT ngram FROM %T WHERE ngram IN (%Ls)', 'UPDATE %T
$engine->getCommonNgramsTableName(), SET
$ngrams); isClosed = %d,
$common = ipull($common, 'ngram', 'ngram'); epochCreated = %d,
epochModified = %d,
authorPHID = %ns,
ownerPHID = %ns
WHERE id = %d',
$engine->getDocumentTableName(),
$is_closed,
$document->getDocumentCreated(),
$document->getDocumentModified(),
$author_phid,
$owner_phid,
$document_id);
foreach ($ngrams as $key => $ngram) { $is_new = false;
if (isset($common[$ngram])) {
unset($ngrams[$key]);
continue;
}
// NOTE: MySQL discards trailing whitespace in CHAR(X) columns.
$trim_ngram = rtrim($ngram, ' ');
if (isset($common[$ngram])) {
unset($ngrams[$key]);
continue;
}
}
} }
if ($ngrams) { $this->updateStoredFields(
$sql = array(); $conn,
foreach ($ngrams as $ngram) { $is_new,
$sql[] = qsprintf( $document_id,
$conn, $engine,
'(%d, %s)', $ferret_fields);
$document_id,
$ngram); $this->updateStoredNgrams(
} $conn,
$is_new,
$document_id,
$engine,
$ngrams);
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn,
'INSERT INTO %T (documentID, ngram) VALUES %LQ',
$engine->getNgramsTableName(),
$chunk);
}
}
} catch (Exception $ex) { } catch (Exception $ex) {
$object->killTransaction(); $object->killTransaction();
throw $ex; throw $ex;
} catch (Throwable $ex) {
$object->killTransaction();
throw $ex;
} }
$object->saveTransaction(); $object->saveTransaction();
} }
private function updateStoredFields(
private function deleteOldDocument( AphrontDatabaseConnection $conn,
$is_new,
$document_id,
PhabricatorFerretEngine $engine, PhabricatorFerretEngine $engine,
$object, $new_fields) {
PhabricatorSearchAbstractDocument $document) {
$conn = $object->establishConnection('w'); if (!$is_new) {
$old_fields = queryfx_all(
$old_document = queryfx_one( $conn,
$conn, 'SELECT * FROM %T WHERE documentID = %d',
'SELECT * FROM %T WHERE objectPHID = %s', $engine->getFieldTableName(),
$engine->getDocumentTableName(), $document_id);
$object->getPHID()); } else {
if (!$old_document) { $old_fields = array();
return;
} }
$old_id = $old_document['id']; $old_fields = ipull($old_fields, null, 'fieldKey');
$new_fields = ipull($new_fields, null, 'fieldKey');
queryfx( $delete_rows = array();
$conn, $insert_rows = array();
'DELETE FROM %T WHERE id = %d', $update_rows = array();
$engine->getDocumentTableName(),
$old_id);
queryfx( foreach ($old_fields as $field_key => $old_field) {
$conn, if (!isset($new_fields[$field_key])) {
'DELETE FROM %T WHERE documentID = %d', $delete_rows[] = $old_field;
$engine->getFieldTableName(), }
$old_id); }
queryfx( $compare_keys = array(
$conn, 'rawCorpus',
'DELETE FROM %T WHERE documentID = %d', 'termCorpus',
$engine->getNgramsTableName(), 'normalCorpus',
$old_id); );
foreach ($new_fields as $field_key => $new_field) {
if (!isset($old_fields[$field_key])) {
$insert_rows[] = $new_field;
continue;
}
$old_field = $old_fields[$field_key];
$same_row = true;
foreach ($compare_keys as $compare_key) {
if ($old_field[$compare_key] !== $new_field[$compare_key]) {
$same_row = false;
break;
}
}
if ($same_row) {
continue;
}
$new_field['id'] = $old_field['id'];
$update_rows[] = $new_field;
}
if ($delete_rows) {
queryfx(
$conn,
'DELETE FROM %T WHERE id IN (%Ld)',
$engine->getFieldTableName(),
ipull($delete_rows, 'id'));
}
foreach ($update_rows as $update_row) {
queryfx(
$conn,
'UPDATE %T
SET
rawCorpus = %s,
termCorpus = %s,
normalCorpus = %s
WHERE id = %d',
$engine->getFieldTableName(),
$update_row['rawCorpus'],
$update_row['termCorpus'],
$update_row['normalCorpus'],
$update_row['id']);
}
foreach ($insert_rows as $insert_row) {
queryfx(
$conn,
'INSERT INTO %T (documentID, fieldKey, rawCorpus, termCorpus,
normalCorpus) VALUES (%d, %s, %s, %s, %s)',
$engine->getFieldTableName(),
$document_id,
$insert_row['fieldKey'],
$insert_row['rawCorpus'],
$insert_row['termCorpus'],
$insert_row['normalCorpus']);
}
}
private function updateStoredNgrams(
AphrontDatabaseConnection $conn,
$is_new,
$document_id,
PhabricatorFerretEngine $engine,
$new_ngrams) {
if ($is_new) {
$old_ngrams = array();
} else {
$old_ngrams = queryfx_all(
$conn,
'SELECT id, ngram FROM %T WHERE documentID = %d',
$engine->getNgramsTableName(),
$document_id);
}
$old_ngrams = ipull($old_ngrams, 'id', 'ngram');
$new_ngrams = array_fuse($new_ngrams);
$delete_ids = array();
$insert_ngrams = array();
// NOTE: MySQL discards trailing whitespace in CHAR(X) columns.
foreach ($old_ngrams as $ngram => $id) {
if (isset($new_ngrams[$ngram])) {
continue;
}
$untrimmed_ngram = $ngram.' ';
if (isset($new_ngrams[$untrimmed_ngram])) {
continue;
}
$delete_ids[] = $id;
}
foreach ($new_ngrams as $ngram) {
if (isset($old_ngrams[$ngram])) {
continue;
}
$trimmed_ngram = rtrim($ngram, ' ');
if (isset($old_ngrams[$trimmed_ngram])) {
continue;
}
$insert_ngrams[] = $ngram;
}
if ($delete_ids) {
$sql = array();
foreach ($delete_ids as $id) {
$sql[] = qsprintf(
$conn,
'%d',
$id);
}
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn,
'DELETE FROM %T WHERE id IN (%LQ)',
$engine->getNgramsTableName(),
$chunk);
}
}
if ($insert_ngrams) {
$sql = array();
foreach ($insert_ngrams as $ngram) {
$sql[] = qsprintf(
$conn,
'(%d, %s)',
$document_id,
$ngram);
}
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn,
'INSERT INTO %T (documentID, ngram) VALUES %LQ',
$engine->getNgramsTableName(),
$chunk);
}
}
} }
public function newFerretSearchFunctions() { public function newFerretSearchFunctions() {

View file

@ -109,8 +109,10 @@ final class PhabricatorIndexEngine extends Phobject {
$rows = queryfx_all( $rows = queryfx_all(
$conn_r, $conn_r,
'SELECT * FROM %T WHERE objectPHID = %s AND extensionKey IN (%Ls)', 'SELECT version, extensionKey
$table->getTableName(), FROM %R
WHERE objectPHID = %s AND extensionKey IN (%Ls)',
$table,
$object_phid, $object_phid,
$extension_keys); $extension_keys);
@ -128,22 +130,35 @@ final class PhabricatorIndexEngine extends Phobject {
$table = new PhabricatorSearchIndexVersion(); $table = new PhabricatorSearchIndexVersion();
$conn_w = $table->establishConnection('w'); $conn_w = $table->establishConnection('w');
$now = PhabricatorTime::getNow();
// See T13587. For now, this is just a marker to make it easy to reindex
// documents if some version of the indexing code is later discovered to
// be questionable.
$index_version = '2021-02-16-A';
$sql = array(); $sql = array();
foreach ($versions as $key => $version) { foreach ($versions as $key => $version) {
$sql[] = qsprintf( $sql[] = qsprintf(
$conn_w, $conn_w,
'(%s, %s, %s)', '(%s, %s, %s, %s, %d)',
$object_phid, $object_phid,
$key, $key,
$version); $version,
$index_version,
$now);
} }
queryfx( queryfx(
$conn_w, $conn_w,
'INSERT INTO %T (objectPHID, extensionKey, version) 'INSERT INTO %R (objectPHID, extensionKey, version,
indexVersion, indexEpoch)
VALUES %LQ VALUES %LQ
ON DUPLICATE KEY UPDATE version = VALUES(version)', ON DUPLICATE KEY UPDATE
$table->getTableName(), version = VALUES(version),
indexVersion = VALUES(indexVersion),
indexEpoch = VALUES(indexEpoch)',
$table,
$sql); $sql);
} }

View file

@ -8,9 +8,13 @@ final class PhabricatorSearchManagementIndexWorkflow
->setName('index') ->setName('index')
->setSynopsis(pht('Build or rebuild search indexes.')) ->setSynopsis(pht('Build or rebuild search indexes.'))
->setExamples( ->setExamples(
"**index** D123\n". implode(
"**index** --type task\n". "\n",
"**index** --all") array(
'**index** D123',
'**index** --all',
'**index** [--type __task__] [--version __version__] ...',
)))
->setArguments( ->setArguments(
array( array(
array( array(
@ -20,6 +24,7 @@ final class PhabricatorSearchManagementIndexWorkflow
array( array(
'name' => 'type', 'name' => 'type',
'param' => 'type', 'param' => 'type',
'repeat' => true,
'help' => pht( 'help' => pht(
'Object types to reindex, like "task", "commit" or "revision".'), 'Object types to reindex, like "task", "commit" or "revision".'),
), ),
@ -37,6 +42,28 @@ final class PhabricatorSearchManagementIndexWorkflow
'Force a complete rebuild of the entire index instead of an '. 'Force a complete rebuild of the entire index instead of an '.
'incremental update.'), 'incremental update.'),
), ),
array(
'name' => 'version',
'param' => 'version',
'repeat' => true,
'help' => pht(
'Reindex objects previously indexed with a particular '.
'version of the indexer.'),
),
array(
'name' => 'min-index-date',
'param' => 'date',
'help' => pht(
'Reindex objects previously indexed on or after a '.
'given date.'),
),
array(
'name' => 'max-index-date',
'param' => 'date',
'help' => pht(
'Reindex objects previously indexed on or before a '.
'given date.'),
),
array( array(
'name' => 'objects', 'name' => 'objects',
'wildcard' => true, 'wildcard' => true,
@ -47,37 +74,46 @@ final class PhabricatorSearchManagementIndexWorkflow
public function execute(PhutilArgumentParser $args) { public function execute(PhutilArgumentParser $args) {
$this->validateClusterSearchConfig(); $this->validateClusterSearchConfig();
$console = PhutilConsole::getConsole();
$is_all = $args->getArg('all'); $is_all = $args->getArg('all');
$is_type = $args->getArg('type');
$is_force = $args->getArg('force'); $is_force = $args->getArg('force');
$obj_names = $args->getArg('objects'); $object_types = $args->getArg('type');
$index_versions = $args->getArg('version');
if ($obj_names && ($is_all || $is_type)) { $min_epoch = $args->getArg('min-index-date');
if ($min_epoch !== null) {
$min_epoch = $this->parseTimeArgument($min_epoch);
}
$max_epoch = $args->getArg('max-index-date');
if ($max_epoch !== null) {
$max_epoch = $this->parseTimeArgument($max_epoch);
}
$object_names = $args->getArg('objects');
$any_constraints =
($object_names) ||
($object_types) ||
($index_versions) ||
($min_epoch) ||
($max_epoch);
if ($is_all && $any_constraints) {
throw new PhutilArgumentUsageException( throw new PhutilArgumentUsageException(
pht( pht(
"You can not name objects to index alongside the '%s' or '%s' flags.", 'You can not use query constraint flags (like "--version", '.
'--all', '"--type", or a list of specific objects) with "--all".'));
'--type')); }
} else if (!$obj_names && !($is_all || $is_type)) {
if (!$is_all && !$any_constraints) {
throw new PhutilArgumentUsageException( throw new PhutilArgumentUsageException(
pht( pht(
"Provide one of '%s', '%s' or a list of object names.", 'Provide a list of objects to index (like "D123"), or a set of '.
'--all', 'query constraint flags (like "--type"), or "--all" to index '.
'--type')); 'all objects.'));
} }
if ($obj_names) {
$phids = $this->loadPHIDsByNames($obj_names);
} else {
$phids = $this->loadPHIDsByTypes($is_type);
}
if (!$phids) {
throw new PhutilArgumentUsageException(pht('Nothing to index!'));
}
if ($args->getArg('background')) { if ($args->getArg('background')) {
$is_background = true; $is_background = true;
@ -87,21 +123,80 @@ final class PhabricatorSearchManagementIndexWorkflow
} }
if (!$is_background) { if (!$is_background) {
echo tsprintf( $this->logInfo(
"**<bg:blue> %s </bg>** %s\n",
pht('NOTE'), pht('NOTE'),
pht( pht(
'Run this workflow with "%s" to queue tasks for the daemon workers.', 'Run this workflow with "--background" to queue tasks for the '.
'--background')); 'daemon workers.'));
} }
$groups = phid_group_by_type($phids); $this->logInfo(
foreach ($groups as $group_type => $group) { pht('SELECT'),
$console->writeOut( pht('Selecting objects to index...'));
"%s\n",
pht('Indexing %d object(s) of type %s.', count($group), $group_type)); $object_phids = null;
if ($object_names) {
$object_phids = $this->loadPHIDsByNames($object_names);
$object_phids = array_fuse($object_phids);
} }
$type_phids = null;
if ($is_all || $object_types) {
$object_map = $this->getIndexableObjectsByTypes($object_types);
$type_phids = array();
foreach ($object_map as $object) {
$iterator = new LiskMigrationIterator($object);
foreach ($iterator as $o) {
$type_phids[] = $o->getPHID();
}
}
$type_phids = array_fuse($type_phids);
}
$index_phids = null;
if ($index_versions || $min_epoch || $max_epoch) {
$index_phids = $this->loadPHIDsByIndexConstraints(
$index_versions,
$min_epoch,
$max_epoch);
$index_phids = array_fuse($index_phids);
}
$working_set = null;
$filter_sets = array(
$object_phids,
$type_phids,
$index_phids,
);
foreach ($filter_sets as $filter_set) {
if ($filter_set === null) {
continue;
}
if ($working_set === null) {
$working_set = $filter_set;
continue;
}
$working_set = array_intersect_key($working_set, $filter_set);
}
$phids = array_keys($working_set);
if (!$phids) {
$this->logWarn(
pht('NO OBJECTS'),
pht('No objects selected to index.'));
return 0;
}
$this->logInfo(
pht('INDEXING'),
pht(
'Indexing %s object(s).',
phutil_count($phids)));
$bar = id(new PhutilConsoleProgressBar()) $bar = id(new PhutilConsoleProgressBar())
->setTotal(count($phids)); ->setTotal(count($phids));
@ -166,8 +261,7 @@ final class PhabricatorSearchManagementIndexWorkflow
if ($track_skips) { if ($track_skips) {
if ($count_updated) { if ($count_updated) {
echo tsprintf( $this->logOkay(
"**<bg:green> %s </bg>** %s\n",
pht('DONE'), pht('DONE'),
pht( pht(
'Updated search indexes for %s document(s).', 'Updated search indexes for %s document(s).',
@ -175,29 +269,25 @@ final class PhabricatorSearchManagementIndexWorkflow
} }
if ($count_skipped) { if ($count_skipped) {
echo tsprintf( $this->logWarn(
"**<bg:yellow> %s </bg>** %s\n",
pht('SKIP'), pht('SKIP'),
pht( pht(
'Skipped %s documents(s) which have not updated since they were '. 'Skipped %s documents(s) which have not updated since they were '.
'last indexed.', 'last indexed.',
new PhutilNumber($count_skipped))); new PhutilNumber($count_skipped)));
echo tsprintf( $this->logInfo(
"**<bg:blue> %s </bg>** %s\n",
pht('NOTE'), pht('NOTE'),
pht( pht(
'Use "--force" to force the index to update these documents.')); 'Use "--force" to force the index to update these documents.'));
} }
} else if ($is_background) { } else if ($is_background) {
echo tsprintf( $this->logOkay(
"**<bg:green> %s </bg>** %s\n",
pht('DONE'), pht('DONE'),
pht( pht(
'Queued %s document(s) for background indexing.', 'Queued %s document(s) for background indexing.',
new PhutilNumber(count($phids)))); new PhutilNumber(count($phids))));
} else { } else {
echo tsprintf( $this->logOkay(
"**<bg:green> %s </bg>** %s\n",
pht('DONE'), pht('DONE'),
pht( pht(
'Forced search index updates for %s document(s).', 'Forced search index updates for %s document(s).',
@ -224,62 +314,100 @@ final class PhabricatorSearchManagementIndexWorkflow
return mpull($objects, 'getPHID'); return mpull($objects, 'getPHID');
} }
private function loadPHIDsByTypes($type) { private function getIndexableObjectsByTypes(array $types) {
$objects = id(new PhutilClassMapQuery()) $objects = id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorIndexableInterface') ->setAncestorClass('PhabricatorIndexableInterface')
->execute(); ->execute();
$normalized_type = phutil_utf8_strtolower($type); $type_map = array();
$normal_map = array();
foreach ($types as $type) {
$normalized_type = phutil_utf8_strtolower($type);
$type_map[$type] = $normalized_type;
$matches = array(); if (isset($normal_map[$normalized_type])) {
$old_type = $normal_map[$normalized_type];
throw new PhutilArgumentUsageException(
pht(
'Type specification "%s" duplicates type specification "%s". '.
'Specify each type only once.',
$type,
$old_type));
}
$normal_map[$normalized_type] = $type;
}
$object_matches = array();
$matches_map = array();
$exact_map = array();
foreach ($objects as $object) { foreach ($objects as $object) {
$object_class = get_class($object); $object_class = get_class($object);
if (!$types) {
$object_matches[$object_class] = $object;
continue;
}
$normalized_class = phutil_utf8_strtolower($object_class); $normalized_class = phutil_utf8_strtolower($object_class);
if ($normalized_class === $normalized_type) { // If a specified type is exactly the name of this class, match it.
$matches = array($object_class => $object); if (isset($normal_map[$normalized_class])) {
break; $object_matches[$object_class] = $object;
$matching_type = $normal_map[$normalized_class];
$matches_map[$matching_type] = array($object_class);
$exact_map[$matching_type] = true;
continue;
} }
if (!strlen($type) || foreach ($type_map as $type => $normalized_type) {
strpos($normalized_class, $normalized_type) !== false) { // If we already have an exact match for this type, don't match it
$matches[$object_class] = $object; // as a substring. An indexable "MothObject" should be selectable
// exactly without also selecting "MammothObject".
if (isset($exact_map[$type])) {
continue;
}
// If the selector isn't a substring of the class name, continue.
if (strpos($normalized_class, $normalized_type) === false) {
continue;
}
$matches_map[$type][] = $object_class;
$object_matches[$object_class] = $object;
} }
} }
if (!$matches) { $all_types = array();
$all_types = array(); foreach ($objects as $object) {
foreach ($objects as $object) { $all_types[] = get_class($object);
$all_types[] = get_class($object); }
sort($all_types);
$type_list = implode(', ', $all_types);
foreach ($type_map as $type => $normalized_type) {
$matches = idx($matches_map, $type);
if (!$matches) {
throw new PhutilArgumentUsageException(
pht(
'Type "%s" matches no indexable objects. '.
'Supported types are: %s.',
$type,
$type_list));
} }
sort($all_types);
throw new PhutilArgumentUsageException( if (count($matches) > 1) {
pht( throw new PhutilArgumentUsageException(
'Type "%s" matches no indexable objects. Supported types are: %s.', pht(
$type, 'Type "%s" matches multiple indexable objects. Use a more '.
implode(', ', $all_types))); 'specific string. Matching objects are: %s.',
} $type,
implode(', ', $matches)));
if ((count($matches) > 1) && strlen($type)) {
throw new PhutilArgumentUsageException(
pht(
'Type "%s" matches multiple indexable objects. Use a more '.
'specific string. Matching object types are: %s.',
$type,
implode(', ', array_keys($matches))));
}
$phids = array();
foreach ($matches as $match) {
$iterator = new LiskMigrationIterator($match);
foreach ($iterator as $object) {
$phids[] = $object->getPHID();
} }
} }
return $phids; return $object_matches;
} }
private function loadIndexVersions($phid) { private function loadIndexVersions($phid) {
@ -294,4 +422,43 @@ final class PhabricatorSearchManagementIndexWorkflow
$phid); $phid);
} }
private function loadPHIDsByIndexConstraints(
array $index_versions,
$min_date,
$max_date) {
$table = new PhabricatorSearchIndexVersion();
$conn = $table->establishConnection('r');
$where = array();
if ($index_versions) {
$where[] = qsprintf(
$conn,
'indexVersion IN (%Ls)',
$index_versions);
}
if ($min_date !== null) {
$where[] = qsprintf(
$conn,
'indexEpoch >= %d',
$min_date);
}
if ($max_date !== null) {
$where[] = qsprintf(
$conn,
'indexEpoch <= %d',
$max_date);
}
$rows = queryfx_all(
$conn,
'SELECT DISTINCT objectPHID FROM %R WHERE %LA',
$table,
$where);
return ipull($rows, 'objectPHID');
}
} }

View file

@ -6,6 +6,8 @@ final class PhabricatorSearchIndexVersion
protected $objectPHID; protected $objectPHID;
protected $extensionKey; protected $extensionKey;
protected $version; protected $version;
protected $indexVersion;
protected $indexEpoch;
protected function getConfiguration() { protected function getConfiguration() {
return array( return array(
@ -13,12 +15,18 @@ final class PhabricatorSearchIndexVersion
self::CONFIG_COLUMN_SCHEMA => array( self::CONFIG_COLUMN_SCHEMA => array(
'extensionKey' => 'text64', 'extensionKey' => 'text64',
'version' => 'text128', 'version' => 'text128',
'indexVersion' => 'bytes12',
'indexEpoch' => 'epoch',
), ),
self::CONFIG_KEY_SCHEMA => array( self::CONFIG_KEY_SCHEMA => array(
'key_object' => array( 'key_object' => array(
'columns' => array('objectPHID', 'extensionKey'), 'columns' => array('objectPHID', 'extensionKey'),
'unique' => true, 'unique' => true,
), ),
// NOTE: "bin/search index" may query this table by "indexVersion" or
// "indexEpoch", but this is rare and scanning the table seems fine.
), ),
) + parent::getConfiguration(); ) + parent::getConfiguration();
} }

View file

@ -62,17 +62,30 @@ final class PhabricatorSubscriptionsCurtainExtension
$handles = $viewer->loadHandles($visible_phids); $handles = $viewer->loadHandles($visible_phids);
} }
PhabricatorPolicyFilterSet::loadHandleViewCapabilities(
$viewer,
$handles,
array($object));
$ref_list = id(new PHUICurtainObjectRefListView()) $ref_list = id(new PHUICurtainObjectRefListView())
->setViewer($viewer) ->setViewer($viewer)
->setEmptyMessage(pht('None')); ->setEmptyMessage(pht('None'));
foreach ($visible_phids as $phid) { foreach ($visible_phids as $phid) {
$handle = $handles[$phid];
$ref = $ref_list->newObjectRefView() $ref = $ref_list->newObjectRefView()
->setHandle($handles[$phid]); ->setHandle($handle);
if ($phid === $viewer_phid) { if ($phid === $viewer_phid) {
$ref->setHighlighted(true); $ref->setHighlighted(true);
} }
if ($handle->hasCapabilities()) {
if (!$handle->hasViewCapability($object)) {
$ref->setExiled(true);
}
}
} }
if ($show_all) { if ($show_all) {

View file

@ -42,7 +42,7 @@ final class PhabricatorSystemDebugUIEventListener
$submenu[] = id(new PhabricatorActionView()) $submenu[] = id(new PhabricatorActionView())
->setIcon('fa-address-card-o') ->setIcon('fa-address-card-o')
->setName(pht('View Hovercard')) ->setName(pht('View Hovercard'))
->setHref(urisprintf('/search/hovercard/?phids[]=%s', $phid)); ->setHref(urisprintf('/search/hovercard/?names=%s', $phid));
$developer_action = id(new PhabricatorActionView()) $developer_action = id(new PhabricatorActionView())
->setName(pht('Advanced/Developer...')) ->setName(pht('Advanced/Developer...'))

View file

@ -84,6 +84,7 @@ abstract class PhabricatorTimelineEngine
return $view return $view
->setViewer($viewer) ->setViewer($viewer)
->setObject($object)
->setObjectPHID($object->getPHID()) ->setObjectPHID($object->getPHID())
->setTransactions($xactions); ->setTransactions($xactions);
} }

View file

@ -9,6 +9,7 @@ class PhabricatorApplicationTransactionView extends AphrontView {
private $engine; private $engine;
private $showEditActions = true; private $showEditActions = true;
private $isPreview; private $isPreview;
private $object;
private $objectPHID; private $objectPHID;
private $shouldTerminate = false; private $shouldTerminate = false;
private $quoteTargetID; private $quoteTargetID;
@ -41,6 +42,16 @@ class PhabricatorApplicationTransactionView extends AphrontView {
return $this->quoteTargetID; return $this->quoteTargetID;
} }
public function setObject(
PhabricatorApplicationTransactionInterface $object) {
$this->object = $object;
return $this;
}
private function getObject() {
return $this->object;
}
public function setObjectPHID($object_phid) { public function setObjectPHID($object_phid) {
$this->objectPHID = $object_phid; $this->objectPHID = $object_phid;
return $this; return $this;
@ -238,6 +249,12 @@ class PhabricatorApplicationTransactionView extends AphrontView {
$engine = id(new PhabricatorMarkupEngine()) $engine = id(new PhabricatorMarkupEngine())
->setViewer($this->getViewer()); ->setViewer($this->getViewer());
$object = $this->getObject();
if ($object) {
$engine->setContextObject($object);
}
foreach ($this->transactions as $xaction) { foreach ($this->transactions as $xaction) {
if (!$xaction->hasComment()) { if (!$xaction->hasComment()) {
continue; continue;

View file

@ -69,7 +69,9 @@ final class ManiphestTaskGraph
'href' => $object->getURI(), 'href' => $object->getURI(),
'sigil' => 'hovercard', 'sigil' => 'hovercard',
'meta' => array( 'meta' => array(
'hoverPHID' => $object->getPHID(), 'hovercardSpec' => array(
'objectPHID' => $object->getPHID(),
),
), ),
), ),
$object->getTitle()); $object->getTitle());

View file

@ -9,18 +9,50 @@ final class PhutilRemarkupHyperlinkRule extends PhutilRemarkupRule {
} }
public function apply($text) { public function apply($text) {
static $angle_pattern;
static $curly_pattern;
static $bare_pattern;
if ($angle_pattern === null) {
// See T13608. A previous version of this code matched bare URIs
// starting with "\w{3,}", which can take a very long time to match
// against long inputs.
//
// Use a protocol length limit in all patterns for general sanity,
// and a negative lookbehind in the bare pattern to avoid explosive
// complexity during expression evaluation.
$protocol_fragment = '\w{3,32}';
$uri_fragment = '[^\s'.PhutilRemarkupBlockStorage::MAGIC_BYTE.']+';
$angle_pattern = sprintf(
'(<(%s://%s?)>)',
$protocol_fragment,
$uri_fragment);
$curly_pattern = sprintf(
'({(%s://%s?)})',
$protocol_fragment,
$uri_fragment);
$bare_pattern = sprintf(
'((?<!\w)%s://%s)',
$protocol_fragment,
$uri_fragment);
}
// Hyperlinks with explicit "<>" around them get linked exactly, without // Hyperlinks with explicit "<>" around them get linked exactly, without
// the "<>". Angle brackets are basically special and mean "this is a URL // the "<>". Angle brackets are basically special and mean "this is a URL
// with weird characters". This is assumed to be reasonable because they // with weird characters". This is assumed to be reasonable because they
// don't appear in normal text or normal URLs. // don't appear in most normal text or most normal URLs.
$text = preg_replace_callback( $text = preg_replace_callback(
'@<(\w{3,}://[^\s'.PhutilRemarkupBlockStorage::MAGIC_BYTE.']+?)>@', $angle_pattern,
array($this, 'markupHyperlinkAngle'), array($this, 'markupHyperlinkAngle'),
$text); $text);
// We match "{uri}", but do not link it by default. // We match "{uri}", but do not link it by default.
$text = preg_replace_callback( $text = preg_replace_callback(
'@{(\w{3,}://[^\s'.PhutilRemarkupBlockStorage::MAGIC_BYTE.']+?)}@', $curly_pattern,
array($this, 'markupHyperlinkCurly'), array($this, 'markupHyperlinkCurly'),
$text); $text);
@ -31,8 +63,9 @@ final class PhutilRemarkupHyperlinkRule extends PhutilRemarkupRule {
// NOTE: We're explicitly avoiding capturing stored blocks, so text like // NOTE: We're explicitly avoiding capturing stored blocks, so text like
// `http://www.example.com/[[x | y]]` doesn't get aggressively captured. // `http://www.example.com/[[x | y]]` doesn't get aggressively captured.
$text = preg_replace_callback( $text = preg_replace_callback(
'@(\w{3,}://[^\s'.PhutilRemarkupBlockStorage::MAGIC_BYTE.']+)@', $bare_pattern,
array($this, 'markupHyperlinkUngreedy'), array($this, 'markupHyperlinkUngreedy'),
$text); $text);
@ -110,7 +143,7 @@ final class PhutilRemarkupHyperlinkRule extends PhutilRemarkupRule {
} }
protected function markupHyperlinkUngreedy($matches) { protected function markupHyperlinkUngreedy($matches) {
$match = $matches[1]; $match = $matches[0];
$tail = null; $tail = null;
$trailing = null; $trailing = null;
if (preg_match('/[;,.:!?]+$/', $match, $trailing)) { if (preg_match('/[;,.:!?]+$/', $match, $trailing)) {

View file

@ -6,6 +6,7 @@ final class PHUICurtainObjectRefView
private $handle; private $handle;
private $epoch; private $epoch;
private $highlighted; private $highlighted;
private $exiled;
public function setHandle(PhabricatorObjectHandle $handle) { public function setHandle(PhabricatorObjectHandle $handle) {
$this->handle = $handle; $this->handle = $handle;
@ -22,6 +23,11 @@ final class PHUICurtainObjectRefView
return $this; return $this;
} }
public function setExiled($is_exiled) {
$this->exiled = $is_exiled;
return $this;
}
protected function getTagAttributes() { protected function getTagAttributes() {
$classes = array(); $classes = array();
$classes[] = 'phui-curtain-object-ref-view'; $classes[] = 'phui-curtain-object-ref-view';
@ -29,6 +35,11 @@ final class PHUICurtainObjectRefView
if ($this->highlighted) { if ($this->highlighted) {
$classes[] = 'phui-curtain-object-ref-view-highlighted'; $classes[] = 'phui-curtain-object-ref-view-highlighted';
} }
if ($this->exiled) {
$classes[] = 'phui-curtain-object-ref-view-exiled';
}
$classes = implode(' ', $classes); $classes = implode(' ', $classes);
return array( return array(
@ -60,6 +71,24 @@ final class PHUICurtainObjectRefView
$more_rows[] = phutil_tag('tr', array(), $epoch_cells); $more_rows[] = phutil_tag('tr', array(), $epoch_cells);
} }
if ($this->exiled) {
$exiled_view = array(
id(new PHUIIconView())->setIcon('fa-eye-slash red'),
' ',
pht('No View Permission'),
);
$exiled_cells = array();
$exiled_cells[] = phutil_tag(
'td',
array(
'class' => 'phui-curtain-object-ref-view-exiled-cell',
),
$exiled_view);
$more_rows[] = phutil_tag('tr', array(), $exiled_cells);
}
$header_cells = array(); $header_cells = array();
$image_view = $this->newImage(); $image_view = $this->newImage();

View file

@ -18,6 +18,7 @@ final class PHUIHovercardView extends AphrontTagView {
private $fields = array(); private $fields = array();
private $actions = array(); private $actions = array();
private $badges = array(); private $badges = array();
private $isExiled;
public function setObjectHandle(PhabricatorObjectHandle $handle) { public function setObjectHandle(PhabricatorObjectHandle $handle) {
$this->handle = $handle; $this->handle = $handle;
@ -43,6 +44,15 @@ final class PHUIHovercardView extends AphrontTagView {
return $this; return $this;
} }
public function setIsExiled($is_exiled) {
$this->isExiled = $is_exiled;
return $this;
}
public function getIsExiled() {
return $this->isExiled;
}
public function addField($label, $value) { public function addField($label, $value) {
$this->fields[] = array( $this->fields[] = array(
'label' => $label, 'label' => $label,

View file

@ -8,6 +8,7 @@ final class PHUIStatusItemView extends AphrontTagView {
private $target; private $target;
private $note; private $note;
private $highlighted; private $highlighted;
private $isExiled;
const ICON_ACCEPT = 'fa-check-circle'; const ICON_ACCEPT = 'fa-check-circle';
const ICON_REJECT = 'fa-times-circle'; const ICON_REJECT = 'fa-times-circle';
@ -46,6 +47,11 @@ final class PHUIStatusItemView extends AphrontTagView {
return $this; return $this;
} }
public function setIsExiled($is_exiled) {
$this->isExiled = $is_exiled;
return $this;
}
protected function canAppendChild() { protected function canAppendChild() {
return false; return false;
} }
@ -60,6 +66,10 @@ final class PHUIStatusItemView extends AphrontTagView {
$classes[] = 'phui-status-item-highlighted'; $classes[] = 'phui-status-item-highlighted';
} }
if ($this->isExiled) {
$classes[] = 'phui-status-item-exiled';
}
return array( return array(
'class' => $classes, 'class' => $classes,
); );

View file

@ -44,6 +44,8 @@ final class PHUITagView extends AphrontTagView {
private $shade; private $shade;
private $slimShady; private $slimShady;
private $border; private $border;
private $contextObject;
private $isExiled;
public function setType($type) { public function setType($type) {
$this->type = $type; $this->type = $type;
@ -127,6 +129,24 @@ final class PHUITagView extends AphrontTagView {
return strlen($this->href) ? 'a' : 'span'; return strlen($this->href) ? 'a' : 'span';
} }
public function setContextObject($context_object) {
$this->contextObject = $context_object;
return $this;
}
public function getContextObject() {
return $this->contextObject;
}
public function setIsExiled($is_exiled) {
$this->isExiled = $is_exiled;
return $this;
}
public function getIsExiled() {
return $this->isExiled;
}
protected function getTagAttributes() { protected function getTagAttributes() {
require_celerity_resource('phui-tag-view-css'); require_celerity_resource('phui-tag-view-css');
@ -155,6 +175,10 @@ final class PHUITagView extends AphrontTagView {
$classes[] = 'phui-tag-'.$this->border; $classes[] = 'phui-tag-'.$this->border;
} }
if ($this->getIsExiled()) {
$classes[] = 'phui-tag-exiled';
}
$attributes = array( $attributes = array(
'href' => $this->href, 'href' => $this->href,
'class' => $classes, 'class' => $classes,
@ -170,10 +194,19 @@ final class PHUITagView extends AphrontTagView {
if ($this->phid) { if ($this->phid) {
Javelin::initBehavior('phui-hovercards'); Javelin::initBehavior('phui-hovercards');
$hovercard_spec = array(
'objectPHID' => $this->phid,
);
$context_object = $this->getContextObject();
if ($context_object) {
$hovercard_spec['contextPHID'] = $context_object->getPHID();
}
$attributes += array( $attributes += array(
'sigil' => 'hovercard', 'sigil' => 'hovercard',
'meta' => array( 'meta' => array(
'hoverPHID' => $this->phid, 'hovercardSpec' => $hovercard_spec,
), ),
); );
} }

View file

@ -72,6 +72,17 @@
color: {$greytext}; color: {$greytext};
} }
.project-card-view .project-card-item-exiled {
background-color: {$lightredbackground};
border-radius: 4px;
padding: 2px 8px;
margin: 2px 0;
}
.project-card-view .project-card-item-exiled .project-card-item-text {
color: {$red};
}
.project-card-view .project-card-item-icon { .project-card-view .project-card-item-icon {
width: 20px; width: 20px;
} }

View file

@ -291,11 +291,6 @@ video.phabricator-media {
color: {$greytext}; color: {$greytext};
} }
.phabricator-remarkup-mention-nopermission .phui-tag-core {
background: {$lightgreybackground};
color: {$lightgreytext};
}
.phabricator-remarkup .remarkup-note { .phabricator-remarkup .remarkup-note {
margin: 16px 0; margin: 16px 0;
padding: 12px; padding: 12px;

View file

@ -12,6 +12,10 @@
border-radius: 3px; border-radius: 3px;
} }
.phui-curtain-object-ref-view + .phui-curtain-object-ref-view {
margin-top: 1px;
}
.phui-curtain-object-ref-view-image-cell { .phui-curtain-object-ref-view-image-cell {
min-width: 32px; min-width: 32px;
padding-bottom: 24px; padding-bottom: 24px;
@ -82,3 +86,12 @@
.phui-curtain-object-ref-view-highlighted { .phui-curtain-object-ref-view-highlighted {
background: {$bluebackground}; background: {$bluebackground};
} }
.phui-curtain-object-ref-view-exiled {
background: {$lightred};
opacity: 0.75;
}
.phui-curtain-object-ref-view-exiled-cell {
color: {$red};
}

View file

@ -29,10 +29,16 @@
border-radius: 3px; border-radius: 3px;
} }
.phui-status-item-exiled td {
background-color: {$lightredbackground};
border-radius: 3px;
}
.phui-status-list-view td a { .phui-status-list-view td a {
color: {$darkbluetext}; color: {$darkbluetext};
} }
.phui-status-item-highlighted td.phui-status-item-note { .phui-status-item-highlighted td.phui-status-item-note,
.phui-status-item-exiled td.phui-status-item-note {
background-color: transparent; background-color: transparent;
} }

View file

@ -531,3 +531,15 @@ a.phui-tag-view:hover.phui-tag-disabled .phui-tag-core {
color: {$blacktext}; color: {$blacktext};
border-color: {$blacktext}; border-color: {$blacktext};
} }
.phui-tag-exiled .phui-tag-core {
border-color: {$lightredborder};
color: {$red};
background: {$lightredbackground};
}
a.phui-tag-view.phui-tag-exiled:hover
.phui-tag-core.phui-tag-color-person {
border-color: {$red};
}

View file

@ -10,163 +10,19 @@
JX.install('Hovercard', { JX.install('Hovercard', {
statics : { properties: {
_node : null, hovercardKey: null,
_activeRoot : null, objectPHID: null,
_visiblePHID : null, contextPHID: null,
_alignment: null, isLoading: false,
isLoaded: false,
content: null
},
fetchUrl : '/search/hovercard/', members: {
newContentNode: function() {
/** return JX.$H(this.getContent());
* Hovercard storage. {"PHID-XXXX-YYYY":"<...>", ...}
*/
_cards : {},
getAnchor : function() {
return this._activeRoot;
},
getCard : function() {
var self = JX.Hovercard;
return self._node;
},
getAlignment: function() {
var self = JX.Hovercard;
return self._alignment;
},
show : function(root, phid) {
var self = JX.Hovercard;
if (root === this._activeRoot) {
return;
}
self.hide();
self._visiblePHID = phid;
self._activeRoot = root;
if (!(phid in self._cards)) {
self._load([phid]);
} else {
self._drawCard(phid);
}
},
_drawCard : function(phid) {
var self = JX.Hovercard;
// card is loading...
if (self._cards[phid] === true) {
return;
}
// Not the current requested card
if (phid != self._visiblePHID) {
return;
}
// Not loaded
if (!(phid in self._cards)) {
return;
}
var root = self._activeRoot;
var node = JX.$N('div',
{ className: 'jx-hovercard-container' },
JX.$H(self._cards[phid]));
self._node = node;
// Append the card to the document, but offscreen, so we can measure it.
node.style.left = '-10000px';
document.body.appendChild(node);
// Retrieve size from child (wrapper), since node gives wrong dimensions?
var child = node.firstChild;
var p = JX.$V(root);
var d = JX.Vector.getDim(root);
var n = JX.Vector.getDim(child);
var v = JX.Vector.getViewport();
var s = JX.Vector.getScroll();
// Move the tip so it's nicely aligned.
var margin = 20;
// Try to align the card directly above the link, with left borders
// touching.
var x = p.x;
// If this would push us off the right side of the viewport, push things
// back to the left.
if ((x + n.x + margin) > (s.x + v.x)) {
x = (s.x + v.x) - n.x - margin;
}
// Try to put the card above the link.
var y = p.y - n.y - margin;
self._alignment = 'north';
// If the card is near the top of the window, show it beneath the
// link we're hovering over instead.
if ((y - margin) < s.y) {
y = p.y + d.y + margin;
self._alignment = 'south';
}
node.style.left = x + 'px';
node.style.top = y + 'px';
},
hide : function() {
var self = JX.Hovercard;
self._visiblePHID = null;
self._activeRoot = null;
if (self._node) {
JX.DOM.remove(self._node);
self._node = null;
}
},
/**
* Pass it an array of phids to load them into storage
*
* @param list phids
*/
_load : function(phids) {
var self = JX.Hovercard;
var uri = JX.$U(self.fetchUrl);
var send = false;
for (var ii = 0; ii < phids.length; ii++) {
var phid = phids[ii];
if (phid in self._cards) {
continue;
}
self._cards[phid] = true; // means "loading"
uri.setQueryParam('phids['+ii+']', phids[ii]);
send = true;
}
if (!send) {
// already loaded / loading everything!
return;
}
new JX.Request(uri, function(r) {
for (var phid in r.cards) {
self._cards[phid] = r.cards[phid];
// Don't draw if the user is faster than the browser
// Only draw if the user is still requesting the original card
if (self.getCard() && phid != self._visiblePHID) {
continue;
}
self._drawCard(phid);
}
}).send();
} }
} }
}); });

View file

@ -0,0 +1,233 @@
/**
* @requires javelin-install
* javelin-dom
* javelin-vector
* javelin-request
* javelin-uri
* phui-hovercard
* @provides phui-hovercard-list
* @javelin
*/
JX.install('HovercardList', {
construct: function() {
this._cards = {};
this._drawRequest = {};
},
members: {
_cardNode: null,
_rootNode: null,
_cards: null,
_drawRequest: null,
_visibleCard: null,
_fetchURI : '/search/hovercard/',
getCard: function(spec) {
var hovercard_key = this._newHovercardKey(spec);
if (!(hovercard_key in this._cards)) {
var card = new JX.Hovercard()
.setHovercardKey(hovercard_key)
.setObjectPHID(spec.objectPHID)
.setContextPHID(spec.contextPHID || null);
this._cards[hovercard_key] = card;
}
return this._cards[hovercard_key];
},
drawCard: function(card, node) {
this._drawRequest = {
card: card,
node: node
};
if (card.getIsLoaded()) {
return this._paintCard(card);
}
if (card.getIsLoading()) {
return;
}
var hovercard_key = card.getHovercardKey();
var request = {};
request[hovercard_key] = this._newCardRequest(card);
request = JX.JSON.stringify(request);
var uri = JX.$U(this._fetchURI)
.setQueryParam('cards', request);
var onresponse = JX.bind(this, function(r) {
var card = this._cards[hovercard_key];
this._fillCard(card, r.cards[hovercard_key]);
this._paintCard(card);
});
card.setIsLoading(true);
new JX.Request(uri, onresponse)
.send();
},
_newHovercardKey: function(spec) {
var parts = [
spec.objectPHID,
spec.contextPHID
];
return parts.join('/');
},
_newCardRequest: function(card) {
return {
objectPHID: card.getObjectPHID(),
contextPHID: card.getContextPHID()
};
},
_getCardNode: function() {
if (!this._cardNode) {
var attributes = {
className: 'jx-hovercard-container'
};
this._cardNode = JX.$N('div', attributes);
}
return this._cardNode;
},
_fillCard: function(card, response) {
card.setContent(response);
card.setIsLoaded(true);
},
_paintCard: function(card) {
var request = this._drawRequest;
if (request.card !== card) {
// This paint request is no longer the most recent paint request.
return;
}
this.hideCard();
this._rootNode = request.node;
var root = this._rootNode;
var node = this._getCardNode();
JX.DOM.setContent(node, card.newContentNode());
// Append the card to the document, but offscreen, so we can measure it.
node.style.left = '-10000px';
document.body.appendChild(node);
// Retrieve size from child (wrapper), since node gives wrong dimensions?
var child = node.firstChild;
var p = JX.$V(root);
var d = JX.Vector.getDim(root);
var n = JX.Vector.getDim(child);
var v = JX.Vector.getViewport();
var s = JX.Vector.getScroll();
// Move the tip so it's nicely aligned.
var margin = 20;
// Try to align the card directly above the link, with left borders
// touching.
var x = p.x;
// If this would push us off the right side of the viewport, push things
// back to the left.
if ((x + n.x + margin) > (s.x + v.x)) {
x = (s.x + v.x) - n.x - margin;
}
// Try to put the card above the link.
var y = p.y - n.y - margin;
var alignment = 'north';
// If the card is near the top of the window, show it beneath the
// link we're hovering over instead.
if ((y - margin) < s.y) {
y = p.y + d.y + margin;
alignment = 'south';
}
this._alignment = alignment;
node.style.left = x + 'px';
node.style.top = y + 'px';
this._visibleCard = card;
},
hideCard: function() {
var node = this._getCardNode();
JX.DOM.remove(node);
this._rootNode = null;
this._alignment = null;
this._visibleCard = null;
},
onMouseMove: function(e) {
if (!this._visibleCard) {
return;
}
var root = this._rootNode;
var node = this._getCardNode();
var alignment = this._alignment;
var mouse = JX.$V(e);
var node_pos = JX.$V(node);
var node_dim = JX.Vector.getDim(node);
var root_pos = JX.$V(root);
var root_dim = JX.Vector.getDim(root);
var margin = 20;
if (alignment === 'south') {
// Cursor is below the node.
if (mouse.y > node_pos.y + node_dim.y + margin) {
this.hideCard();
}
// Cursor is above the root.
if (mouse.y < root_pos.y - margin) {
this.hideCard();
}
} else {
// Cursor is above the node.
if (mouse.y < node_pos.y - margin) {
this.hideCard();
}
// Cursor is below the root.
if (mouse.y > root_pos.y + root_dim.y + margin) {
this.hideCard();
}
}
// Cursor is too far to the left.
if (mouse.x < Math.min(root_pos.x, node_pos.x) - margin) {
this.hideCard();
}
// Cursor is too far to the right.
if (mouse.x >
Math.max(root_pos.x + root_dim.x, node_pos.x + node_dim.x) + margin) {
this.hideCard();
}
}
}
});

View file

@ -5,10 +5,18 @@
* javelin-stratcom * javelin-stratcom
* javelin-vector * javelin-vector
* phui-hovercard * phui-hovercard
* phui-hovercard-list
* @javelin * @javelin
*/ */
JX.behavior('phui-hovercards', function() { JX.behavior('phui-hovercards', function(config, statics) {
if (statics.hovercardList) {
return;
}
var cards = new JX.HovercardList();
statics.hovercardList = cards;
// We listen for mousemove instead of mouseover to handle the case when user // We listen for mousemove instead of mouseover to handle the case when user
// scrolls with keyboard. We don't want to display hovercard if node gets // scrolls with keyboard. We don't want to display hovercard if node gets
@ -23,65 +31,19 @@ JX.behavior('phui-hovercards', function() {
return; return;
} }
var data = e.getNodeData('hovercard'); var node = e.getNode('hovercard');
var data = e.getNodeData('hovercard').hovercardSpec;
JX.Hovercard.show( var card = cards.getCard(data);
e.getNode('hovercard'),
data.hoverPHID); cards.drawCard(card, node);
}); });
JX.Stratcom.listen( JX.Stratcom.listen(
'mousemove', 'mousemove',
null, null,
function (e) { function (e) {
if (!JX.Hovercard.getCard()) { cards.onMouseMove(e);
return;
}
var root = JX.Hovercard.getAnchor();
var node = JX.Hovercard.getCard();
var align = JX.Hovercard.getAlignment();
var mouse = JX.$V(e);
var node_pos = JX.$V(node);
var node_dim = JX.Vector.getDim(node);
var root_pos = JX.$V(root);
var root_dim = JX.Vector.getDim(root);
var margin = 20;
if (align == 'south') {
// Cursor is below the node.
if (mouse.y > node_pos.y + node_dim.y + margin) {
JX.Hovercard.hide();
}
// Cursor is above the root.
if (mouse.y < root_pos.y - margin) {
JX.Hovercard.hide();
}
} else {
// Cursor is above the node.
if (mouse.y < node_pos.y - margin) {
JX.Hovercard.hide();
}
// Cursor is below the root.
if (mouse.y > root_pos.y + root_dim.y + margin) {
JX.Hovercard.hide();
}
}
// Cursor is too far to the left.
if (mouse.x < Math.min(root_pos.x, node_pos.x) - margin) {
JX.Hovercard.hide();
}
// Cursor is too far to the right.
if (mouse.x >
Math.max(root_pos.x + root_dim.x, node_pos.x + node_dim.x) + margin) {
JX.Hovercard.hide();
}
}); });
// When we leave the page, hide any visible hovercards. If we don't do this, // When we leave the page, hide any visible hovercards. If we don't do this,
@ -91,7 +53,7 @@ JX.behavior('phui-hovercards', function() {
['unload', 'onresize'], ['unload', 'onresize'],
null, null,
function() { function() {
JX.Hovercard.hide(); cards.hideCard();
}); });
}); });