diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 32ccb9e5f9..01dd75c901 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,8 +9,8 @@ return array( 'names' => array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', - 'core.pkg.css' => 'e4f098a5', - 'core.pkg.js' => 'bd19de1c', + 'core.pkg.css' => '2fa91e14', + 'core.pkg.js' => 'dc13d4b7', 'darkconsole.pkg.js' => '1f9a31bc', 'differential.pkg.css' => '113e692c', 'differential.pkg.js' => 'f6d809c0', @@ -31,7 +31,7 @@ return array( 'rsrc/css/aphront/multi-column.css' => '84cc6640', 'rsrc/css/aphront/notification.css' => '457861ec', 'rsrc/css/aphront/panel-view.css' => '8427b78d', - 'rsrc/css/aphront/phabricator-nav-view.css' => '028126f6', + 'rsrc/css/aphront/phabricator-nav-view.css' => 'a9e3e6d5', 'rsrc/css/aphront/table-view.css' => '8c9bbafe', 'rsrc/css/aphront/tokenizer.css' => '15d5ff71', 'rsrc/css/aphront/tooltip.css' => '173b9431', @@ -498,7 +498,7 @@ return array( 'rsrc/js/core/behavior-more.js' => 'a80d0378', 'rsrc/js/core/behavior-object-selector.js' => '77c1f0b0', 'rsrc/js/core/behavior-oncopy.js' => '2926fff2', - 'rsrc/js/core/behavior-phabricator-nav.js' => '81144dfa', + 'rsrc/js/core/behavior-phabricator-nav.js' => '836f966d', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'acd29eee', 'rsrc/js/core/behavior-read-only-warning.js' => 'ba158207', 'rsrc/js/core/behavior-refresh-csrf.js' => 'ab2f381b', @@ -657,7 +657,7 @@ return array( 'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0', 'javelin-behavior-phabricator-keyboard-shortcuts' => '01fca1f0', 'javelin-behavior-phabricator-line-linker' => '1499a8cb', - 'javelin-behavior-phabricator-nav' => '81144dfa', + 'javelin-behavior-phabricator-nav' => '836f966d', 'javelin-behavior-phabricator-notification-example' => '8ce821c5', 'javelin-behavior-phabricator-object-selector' => '77c1f0b0', 'javelin-behavior-phabricator-oncopy' => '2926fff2', @@ -789,7 +789,7 @@ return array( 'phabricator-keyboard-shortcut' => '1ae869f2', 'phabricator-keyboard-shortcut-manager' => 'c19dd9b9', 'phabricator-main-menu-view' => '1802a242', - 'phabricator-nav-view-css' => '028126f6', + 'phabricator-nav-view-css' => 'a9e3e6d5', 'phabricator-notification' => '008faf9c', 'phabricator-notification-css' => '457861ec', 'phabricator-notification-menu-css' => '10685bd4', @@ -1563,7 +1563,11 @@ return array( 'phuix-icon-view', 'phabricator-prefab', ), - '81144dfa' => array( + '834a1173' => array( + 'javelin-behavior', + 'javelin-scrollbar', + ), + '836f966d' => array( 'javelin-behavior', 'javelin-behavior-device', 'javelin-stratcom', @@ -1573,10 +1577,6 @@ return array( 'javelin-request', 'javelin-util', ), - '834a1173' => array( - 'javelin-behavior', - 'javelin-scrollbar', - ), '8499b6ab' => array( 'javelin-behavior', 'javelin-dom', diff --git a/resources/sql/autopatches/20180218.fact.01.dim.key.sql b/resources/sql/autopatches/20180218.fact.01.dim.key.sql new file mode 100644 index 0000000000..3a81915026 --- /dev/null +++ b/resources/sql/autopatches/20180218.fact.01.dim.key.sql @@ -0,0 +1,5 @@ +CREATE TABLE {$NAMESPACE}_fact.fact_keydimension ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + factKey VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}, + UNIQUE KEY `key_factkey` (factKey) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20180218.fact.02.dim.obj.sql b/resources/sql/autopatches/20180218.fact.02.dim.obj.sql new file mode 100644 index 0000000000..6b38062b29 --- /dev/null +++ b/resources/sql/autopatches/20180218.fact.02.dim.obj.sql @@ -0,0 +1,5 @@ +CREATE TABLE {$NAMESPACE}_fact.fact_objectdimension ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + objectPHID VARBINARY(64) NOT NULL, + UNIQUE KEY `key_object` (objectPHID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20180218.fact.03.data.int.sql b/resources/sql/autopatches/20180218.fact.03.data.int.sql new file mode 100644 index 0000000000..d93d546733 --- /dev/null +++ b/resources/sql/autopatches/20180218.fact.03.data.int.sql @@ -0,0 +1,8 @@ +CREATE TABLE {$NAMESPACE}_fact.fact_intdatapoint ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + keyID INT UNSIGNED NOT NULL, + objectID INT UNSIGNED NOT NULL, + dimensionID INT UNSIGNED, + value BIGINT SIGNED NOT NULL, + epoch INT UNSIGNED NOT NULL +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 29a957ae6e..4455ad139a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2588,6 +2588,7 @@ phutil_register_library_map(array( 'PhabricatorCoreCreateTransaction' => 'applications/transactions/xaction/PhabricatorCoreCreateTransaction.php', 'PhabricatorCoreTransactionType' => 'applications/transactions/xaction/PhabricatorCoreTransactionType.php', 'PhabricatorCoreVoidTransaction' => 'applications/transactions/xaction/PhabricatorCoreVoidTransaction.php', + 'PhabricatorCountFact' => 'applications/fact/fact/PhabricatorCountFact.php', 'PhabricatorCountdown' => 'applications/countdown/storage/PhabricatorCountdown.php', 'PhabricatorCountdownApplication' => 'applications/countdown/application/PhabricatorCountdownApplication.php', 'PhabricatorCountdownController' => 'applications/countdown/controller/PhabricatorCountdownController.php', @@ -2616,6 +2617,9 @@ phutil_register_library_map(array( 'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php', 'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php', 'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php', + 'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php', + 'PhabricatorCustomFieldApplicationSearchDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php', + 'PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource.php', 'PhabricatorCustomFieldAttachment' => 'infrastructure/customfield/field/PhabricatorCustomFieldAttachment.php', 'PhabricatorCustomFieldConfigOptionType' => 'infrastructure/customfield/config/PhabricatorCustomFieldConfigOptionType.php', 'PhabricatorCustomFieldDataNotAvailableException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php', @@ -2907,27 +2911,29 @@ phutil_register_library_map(array( 'PhabricatorExternalAccountsSettingsPanel' => 'applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php', 'PhabricatorExtraConfigSetupCheck' => 'applications/config/check/PhabricatorExtraConfigSetupCheck.php', 'PhabricatorFacebookAuthProvider' => 'applications/auth/provider/PhabricatorFacebookAuthProvider.php', + 'PhabricatorFact' => 'applications/fact/fact/PhabricatorFact.php', 'PhabricatorFactAggregate' => 'applications/fact/storage/PhabricatorFactAggregate.php', 'PhabricatorFactApplication' => 'applications/fact/application/PhabricatorFactApplication.php', 'PhabricatorFactChartController' => 'applications/fact/controller/PhabricatorFactChartController.php', 'PhabricatorFactController' => 'applications/fact/controller/PhabricatorFactController.php', - 'PhabricatorFactCountEngine' => 'applications/fact/engine/PhabricatorFactCountEngine.php', 'PhabricatorFactCursor' => 'applications/fact/storage/PhabricatorFactCursor.php', 'PhabricatorFactDAO' => 'applications/fact/storage/PhabricatorFactDAO.php', 'PhabricatorFactDaemon' => 'applications/fact/daemon/PhabricatorFactDaemon.php', + 'PhabricatorFactDatapointQuery' => 'applications/fact/query/PhabricatorFactDatapointQuery.php', + 'PhabricatorFactDimension' => 'applications/fact/storage/PhabricatorFactDimension.php', 'PhabricatorFactEngine' => 'applications/fact/engine/PhabricatorFactEngine.php', 'PhabricatorFactEngineTestCase' => 'applications/fact/engine/__tests__/PhabricatorFactEngineTestCase.php', 'PhabricatorFactHomeController' => 'applications/fact/controller/PhabricatorFactHomeController.php', - 'PhabricatorFactLastUpdatedEngine' => 'applications/fact/engine/PhabricatorFactLastUpdatedEngine.php', + 'PhabricatorFactIntDatapoint' => 'applications/fact/storage/PhabricatorFactIntDatapoint.php', + 'PhabricatorFactKeyDimension' => 'applications/fact/storage/PhabricatorFactKeyDimension.php', 'PhabricatorFactManagementAnalyzeWorkflow' => 'applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php', 'PhabricatorFactManagementCursorsWorkflow' => 'applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php', 'PhabricatorFactManagementDestroyWorkflow' => 'applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php', 'PhabricatorFactManagementListWorkflow' => 'applications/fact/management/PhabricatorFactManagementListWorkflow.php', - 'PhabricatorFactManagementStatusWorkflow' => 'applications/fact/management/PhabricatorFactManagementStatusWorkflow.php', 'PhabricatorFactManagementWorkflow' => 'applications/fact/management/PhabricatorFactManagementWorkflow.php', + 'PhabricatorFactObjectController' => 'applications/fact/controller/PhabricatorFactObjectController.php', + 'PhabricatorFactObjectDimension' => 'applications/fact/storage/PhabricatorFactObjectDimension.php', 'PhabricatorFactRaw' => 'applications/fact/storage/PhabricatorFactRaw.php', - 'PhabricatorFactSimpleSpec' => 'applications/fact/spec/PhabricatorFactSimpleSpec.php', - 'PhabricatorFactSpec' => 'applications/fact/spec/PhabricatorFactSpec.php', 'PhabricatorFactUpdateIterator' => 'applications/fact/extract/PhabricatorFactUpdateIterator.php', 'PhabricatorFavoritesApplication' => 'applications/favorites/application/PhabricatorFavoritesApplication.php', 'PhabricatorFavoritesController' => 'applications/favorites/controller/PhabricatorFavoritesController.php', @@ -3040,6 +3046,7 @@ phutil_register_library_map(array( 'PhabricatorFilesOnDiskBuiltinFile' => 'applications/files/builtin/PhabricatorFilesOnDiskBuiltinFile.php', 'PhabricatorFilesOutboundRequestAction' => 'applications/files/action/PhabricatorFilesOutboundRequestAction.php', 'PhabricatorFiletreeVisibleSetting' => 'applications/settings/setting/PhabricatorFiletreeVisibleSetting.php', + 'PhabricatorFiletreeWidthSetting' => 'applications/settings/setting/PhabricatorFiletreeWidthSetting.php', 'PhabricatorFlag' => 'applications/flag/storage/PhabricatorFlag.php', 'PhabricatorFlagAddFlagHeraldAction' => 'applications/flag/herald/PhabricatorFlagAddFlagHeraldAction.php', 'PhabricatorFlagColor' => 'applications/flag/constants/PhabricatorFlagColor.php', @@ -3260,6 +3267,7 @@ phutil_register_library_map(array( 'PhabricatorManagementWorkflow' => 'infrastructure/management/PhabricatorManagementWorkflow.php', 'PhabricatorManiphestApplication' => 'applications/maniphest/application/PhabricatorManiphestApplication.php', 'PhabricatorManiphestConfigOptions' => 'applications/maniphest/config/PhabricatorManiphestConfigOptions.php', + 'PhabricatorManiphestTaskFactEngine' => 'applications/fact/engine/PhabricatorManiphestTaskFactEngine.php', 'PhabricatorManiphestTaskTestDataGenerator' => 'applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php', 'PhabricatorManualActivitySetupCheck' => 'applications/config/check/PhabricatorManualActivitySetupCheck.php', 'PhabricatorMarkupCache' => 'applications/cache/storage/PhabricatorMarkupCache.php', @@ -3716,6 +3724,7 @@ phutil_register_library_map(array( 'PhabricatorPirateEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorPirateEnglishTranslation.php', 'PhabricatorPlatformSite' => 'aphront/site/PhabricatorPlatformSite.php', 'PhabricatorPointsEditField' => 'applications/transactions/editfield/PhabricatorPointsEditField.php', + 'PhabricatorPointsFact' => 'applications/fact/fact/PhabricatorPointsFact.php', 'PhabricatorPolicies' => 'applications/policy/constants/PhabricatorPolicies.php', 'PhabricatorPolicy' => 'applications/policy/storage/PhabricatorPolicy.php', 'PhabricatorPolicyApplication' => 'applications/policy/application/PhabricatorPolicyApplication.php', @@ -4357,6 +4366,7 @@ phutil_register_library_map(array( 'PhabricatorTokensSettingsPanel' => 'applications/settings/panel/PhabricatorTokensSettingsPanel.php', 'PhabricatorTokensToken' => 'applications/tokens/storage/PhabricatorTokensToken.php', 'PhabricatorTransactionChange' => 'applications/transactions/data/PhabricatorTransactionChange.php', + 'PhabricatorTransactionFactEngine' => 'applications/fact/engine/PhabricatorTransactionFactEngine.php', 'PhabricatorTransactionRemarkupChange' => 'applications/transactions/data/PhabricatorTransactionRemarkupChange.php', 'PhabricatorTransactions' => 'applications/transactions/constants/PhabricatorTransactions.php', 'PhabricatorTransactionsApplication' => 'applications/transactions/application/PhabricatorTransactionsApplication.php', @@ -4381,6 +4391,7 @@ phutil_register_library_map(array( 'PhabricatorTypeaheadInvalidTokenException' => 'applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php', 'PhabricatorTypeaheadModularDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php', 'PhabricatorTypeaheadMonogramDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php', + 'PhabricatorTypeaheadProxyDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php', 'PhabricatorTypeaheadResult' => 'applications/typeahead/storage/PhabricatorTypeaheadResult.php', 'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadRuntimeCompositeDatasource.php', 'PhabricatorTypeaheadTestNumbersDatasource' => 'applications/typeahead/datasource/__tests__/PhabricatorTypeaheadTestNumbersDatasource.php', @@ -8076,6 +8087,7 @@ phutil_register_library_map(array( 'PhabricatorCoreCreateTransaction' => 'PhabricatorCoreTransactionType', 'PhabricatorCoreTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorCoreVoidTransaction' => 'PhabricatorModularTransactionType', + 'PhabricatorCountFact' => 'PhabricatorFact', 'PhabricatorCountdown' => array( 'PhabricatorCountdownDAO', 'PhabricatorPolicyInterface', @@ -8115,6 +8127,9 @@ phutil_register_library_map(array( 'PhabricatorCountdownViewController' => 'PhabricatorCountdownController', 'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery', 'PhabricatorCustomField' => 'Phobject', + 'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorCustomFieldApplicationSearchDatasource' => 'PhabricatorTypeaheadProxyDatasource', + 'PhabricatorCustomFieldApplicationSearchNoneFunctionDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorCustomFieldAttachment' => 'Phobject', 'PhabricatorCustomFieldConfigOptionType' => 'PhabricatorConfigOptionType', 'PhabricatorCustomFieldDataNotAvailableException' => 'Exception', @@ -8433,27 +8448,29 @@ phutil_register_library_map(array( 'PhabricatorExternalAccountsSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorExtraConfigSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorFacebookAuthProvider' => 'PhabricatorOAuth2AuthProvider', + 'PhabricatorFact' => 'Phobject', 'PhabricatorFactAggregate' => 'PhabricatorFactDAO', 'PhabricatorFactApplication' => 'PhabricatorApplication', 'PhabricatorFactChartController' => 'PhabricatorFactController', 'PhabricatorFactController' => 'PhabricatorController', - 'PhabricatorFactCountEngine' => 'PhabricatorFactEngine', 'PhabricatorFactCursor' => 'PhabricatorFactDAO', 'PhabricatorFactDAO' => 'PhabricatorLiskDAO', 'PhabricatorFactDaemon' => 'PhabricatorDaemon', + 'PhabricatorFactDatapointQuery' => 'Phobject', + 'PhabricatorFactDimension' => 'PhabricatorFactDAO', 'PhabricatorFactEngine' => 'Phobject', 'PhabricatorFactEngineTestCase' => 'PhabricatorTestCase', 'PhabricatorFactHomeController' => 'PhabricatorFactController', - 'PhabricatorFactLastUpdatedEngine' => 'PhabricatorFactEngine', + 'PhabricatorFactIntDatapoint' => 'PhabricatorFactDAO', + 'PhabricatorFactKeyDimension' => 'PhabricatorFactDimension', 'PhabricatorFactManagementAnalyzeWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementCursorsWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementDestroyWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementListWorkflow' => 'PhabricatorFactManagementWorkflow', - 'PhabricatorFactManagementStatusWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementWorkflow' => 'PhabricatorManagementWorkflow', + 'PhabricatorFactObjectController' => 'PhabricatorFactController', + 'PhabricatorFactObjectDimension' => 'PhabricatorFactDimension', 'PhabricatorFactRaw' => 'PhabricatorFactDAO', - 'PhabricatorFactSimpleSpec' => 'PhabricatorFactSpec', - 'PhabricatorFactSpec' => 'Phobject', 'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator', 'PhabricatorFavoritesApplication' => 'PhabricatorApplication', 'PhabricatorFavoritesController' => 'PhabricatorController', @@ -8597,6 +8614,7 @@ phutil_register_library_map(array( 'PhabricatorFilesOnDiskBuiltinFile' => 'PhabricatorFilesBuiltinFile', 'PhabricatorFilesOutboundRequestAction' => 'PhabricatorSystemAction', 'PhabricatorFiletreeVisibleSetting' => 'PhabricatorInternalSetting', + 'PhabricatorFiletreeWidthSetting' => 'PhabricatorInternalSetting', 'PhabricatorFlag' => array( 'PhabricatorFlagDAO', 'PhabricatorPolicyInterface', @@ -8824,6 +8842,7 @@ phutil_register_library_map(array( 'PhabricatorManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhabricatorManiphestApplication' => 'PhabricatorApplication', 'PhabricatorManiphestConfigOptions' => 'PhabricatorApplicationConfigOptions', + 'PhabricatorManiphestTaskFactEngine' => 'PhabricatorTransactionFactEngine', 'PhabricatorManiphestTaskTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorManualActivitySetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorMarkupCache' => 'PhabricatorCacheDAO', @@ -9373,6 +9392,7 @@ phutil_register_library_map(array( 'PhabricatorPirateEnglishTranslation' => 'PhutilTranslation', 'PhabricatorPlatformSite' => 'PhabricatorSite', 'PhabricatorPointsEditField' => 'PhabricatorEditField', + 'PhabricatorPointsFact' => 'PhabricatorFact', 'PhabricatorPolicies' => 'PhabricatorPolicyConstants', 'PhabricatorPolicy' => array( 'PhabricatorPolicyDAO', @@ -10146,6 +10166,7 @@ phutil_register_library_map(array( 'PhabricatorConduitResultInterface', ), 'PhabricatorTransactionChange' => 'Phobject', + 'PhabricatorTransactionFactEngine' => 'PhabricatorFactEngine', 'PhabricatorTransactionRemarkupChange' => 'PhabricatorTransactionChange', 'PhabricatorTransactions' => 'Phobject', 'PhabricatorTransactionsApplication' => 'PhabricatorApplication', @@ -10170,6 +10191,7 @@ phutil_register_library_map(array( 'PhabricatorTypeaheadInvalidTokenException' => 'Exception', 'PhabricatorTypeaheadModularDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadMonogramDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorTypeaheadProxyDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorTypeaheadResult' => 'Phobject', 'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorTypeaheadTestNumbersDatasource' => 'PhabricatorTypeaheadDatasource', diff --git a/src/applications/auth/view/PhabricatorAuthAccountView.php b/src/applications/auth/view/PhabricatorAuthAccountView.php index 8eb73144aa..3f04281c8f 100644 --- a/src/applications/auth/view/PhabricatorAuthAccountView.php +++ b/src/applications/auth/view/PhabricatorAuthAccountView.php @@ -77,6 +77,7 @@ final class PhabricatorAuthAccountView extends AphrontView { array( 'href' => $account_uri, 'target' => '_blank', + 'rel' => 'noreferrer', ), $account_uri); } diff --git a/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php index 6174603c84..bd52ec5bc2 100644 --- a/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php @@ -45,6 +45,7 @@ final class PhabricatorCalendarICSURIImportEngine array( 'href' => $uri, 'target' => '_blank', + 'rel' => 'noreferrer', ), $uri); } diff --git a/src/applications/differential/application/PhabricatorDifferentialApplication.php b/src/applications/differential/application/PhabricatorDifferentialApplication.php index 1c8926f585..4141cf8539 100644 --- a/src/applications/differential/application/PhabricatorDifferentialApplication.php +++ b/src/applications/differential/application/PhabricatorDifferentialApplication.php @@ -35,12 +35,6 @@ final class PhabricatorDifferentialApplication extends PhabricatorApplication { ); } - public function getFactObjectsForAnalysis() { - return array( - new DifferentialRevision(), - ); - } - public function getTitleGlyph() { return "\xE2\x9A\x99"; } diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index e1da5ed32e..d7280cb0b6 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -473,10 +473,14 @@ final class DifferentialRevisionViewController extends DifferentialController { $collapsed_key = PhabricatorFiletreeVisibleSetting::SETTINGKEY; $collapsed_value = $viewer->getUserSetting($collapsed_key); + $width_key = PhabricatorFiletreeWidthSetting::SETTINGKEY; + $width_value = $viewer->getUserSetting($width_key); + $nav = id(new DifferentialChangesetFileTreeSideNavBuilder()) ->setTitle($monogram) ->setBaseURI(new PhutilURI($revision->getURI())) ->setCollapsed((bool)$collapsed_value) + ->setWidth((int)$width_value) ->build($changesets); } diff --git a/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php b/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php index 1f699be8eb..e8852a5c52 100644 --- a/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php +++ b/src/applications/differential/view/DifferentialChangesetFileTreeSideNavBuilder.php @@ -6,6 +6,7 @@ final class DifferentialChangesetFileTreeSideNavBuilder extends Phobject { private $baseURI; private $anchorName; private $collapsed = false; + private $width; public function setAnchorName($anchor_name) { $this->anchorName = $anchor_name; @@ -36,13 +37,19 @@ final class DifferentialChangesetFileTreeSideNavBuilder extends Phobject { return $this; } + public function setWidth($width) { + $this->width = $width; + return $this; + } + public function build(array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI($this->getBaseURI()); - $nav->setFlexible(true); - $nav->setCollapsed($this->collapsed); + $nav = id(new AphrontSideNavFilterView()) + ->setBaseURI($this->getBaseURI()) + ->setFlexible(true) + ->setCollapsed($this->collapsed) + ->setWidth($this->width); $anchor = $this->getAnchorName(); diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php index e619ecb1ad..d42e58b747 100644 --- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php +++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php @@ -39,12 +39,6 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication { ); } - public function getFactObjectsForAnalysis() { - return array( - new PhabricatorRepositoryCommit(), - ); - } - public function getRemarkupRules() { return array( new DiffusionCommitRemarkupRule(), diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index b4d06989c2..67ffb73357 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -415,17 +415,21 @@ final class DiffusionCommitController extends DiffusionController { PhabricatorShowFiletreeSetting::SETTINGKEY, PhabricatorShowFiletreeSetting::VALUE_ENABLE_FILETREE); - $pref_collapse = PhabricatorFiletreeVisibleSetting::SETTINGKEY; - $collapsed = $viewer->getUserSetting($pref_collapse); - $nav = null; if ($show_changesets && $filetree_on) { + $pref_collapse = PhabricatorFiletreeVisibleSetting::SETTINGKEY; + $collapsed = $viewer->getUserSetting($pref_collapse); + + $pref_width = PhabricatorFiletreeWidthSetting::SETTINGKEY; + $width = $viewer->getUserSetting($pref_width); + $nav = id(new DifferentialChangesetFileTreeSideNavBuilder()) ->setTitle($commit->getDisplayName()) ->setBaseURI(new PhutilURI($commit->getURI())) ->build($changesets) ->setCrumbs($crumbs) - ->setCollapsed((bool)$collapsed); + ->setCollapsed((bool)$collapsed) + ->setWidth((int)$width); } $view = id(new PHUITwoColumnView()) diff --git a/src/applications/fact/application/PhabricatorFactApplication.php b/src/applications/fact/application/PhabricatorFactApplication.php index 305ed3abc9..6444fe700a 100644 --- a/src/applications/fact/application/PhabricatorFactApplication.php +++ b/src/applications/fact/application/PhabricatorFactApplication.php @@ -31,6 +31,7 @@ final class PhabricatorFactApplication extends PhabricatorApplication { '/fact/' => array( '' => 'PhabricatorFactHomeController', 'chart/' => 'PhabricatorFactChartController', + 'object/(?[^/]+)/' => 'PhabricatorFactObjectController', ), ); } diff --git a/src/applications/fact/controller/PhabricatorFactChartController.php b/src/applications/fact/controller/PhabricatorFactChartController.php index 16df8fca69..7612cf1568 100644 --- a/src/applications/fact/controller/PhabricatorFactChartController.php +++ b/src/applications/fact/controller/PhabricatorFactChartController.php @@ -5,27 +5,35 @@ final class PhabricatorFactChartController extends PhabricatorFactController { public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); - $table = new PhabricatorFactRaw(); + $series = $request->getStr('y1'); + + $facts = PhabricatorFact::getAllFacts(); + $fact = idx($facts, $series); + + if (!$fact) { + return new Aphront404Response(); + } + + $key_id = id(new PhabricatorFactKeyDimension()) + ->newDimensionID($fact->getKey()); + if (!$key_id) { + return new Aphront404Response(); + } + + $table = $fact->newDatapoint(); $conn_r = $table->establishConnection('r'); $table_name = $table->getTableName(); - $series = $request->getStr('y1'); - - $specs = PhabricatorFactSpec::newSpecsForFactTypes( - PhabricatorFactEngine::loadAllEngines(), - array($series)); - $spec = idx($specs, $series); - $data = queryfx_all( $conn_r, - 'SELECT valueX, epoch FROM %T WHERE factType = %s ORDER BY epoch ASC', + 'SELECT value, epoch FROM %T WHERE keyID = %d ORDER BY epoch ASC', $table_name, - $series); + $key_id); $points = array(); $sum = 0; foreach ($data as $key => $row) { - $sum += (int)$row['valueX']; + $sum += (int)$row['value']; $points[(int)$row['epoch']] = $sum; } @@ -71,7 +79,7 @@ final class PhabricatorFactChartController extends PhabricatorFactController { )); $box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Count of %s', $spec->getName())) + ->setHeaderText(pht('Count of %s', $fact->getName())) ->appendChild($chart); $crumbs = $this->buildApplicationCrumbs(); diff --git a/src/applications/fact/controller/PhabricatorFactHomeController.php b/src/applications/fact/controller/PhabricatorFactHomeController.php index f4eeca0387..82f6a0905b 100644 --- a/src/applications/fact/controller/PhabricatorFactHomeController.php +++ b/src/applications/fact/controller/PhabricatorFactHomeController.php @@ -15,45 +15,6 @@ final class PhabricatorFactHomeController extends PhabricatorFactController { return id(new AphrontRedirectResponse())->setURI($uri); } - $types = array( - '+N:*', - '+N:DREV', - 'updated', - ); - - $engines = PhabricatorFactEngine::loadAllEngines(); - $specs = PhabricatorFactSpec::newSpecsForFactTypes($engines, $types); - - $facts = id(new PhabricatorFactAggregate())->loadAllWhere( - 'factType IN (%Ls)', - $types); - - $rows = array(); - foreach ($facts as $fact) { - $spec = $specs[$fact->getFactType()]; - - $name = $spec->getName(); - $value = $spec->formatValueForDisplay($viewer, $fact->getValueX()); - - $rows[] = array($name, $value); - } - - $table = new AphrontTableView($rows); - $table->setHeaders( - array( - pht('Fact'), - pht('Value'), - )); - $table->setColumnClasses( - array( - 'wide', - 'n', - )); - - $panel = new PHUIObjectBoxView(); - $panel->setHeaderText(pht('Facts')); - $panel->setTable($table); - $chart_form = $this->buildChartForm(); $crumbs = $this->buildApplicationCrumbs(); @@ -64,46 +25,18 @@ final class PhabricatorFactHomeController extends PhabricatorFactController { return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) - ->appendChild(array( - $chart_form, - $panel, - )); - + ->appendChild( + array( + $chart_form, + )); } private function buildChartForm() { $request = $this->getRequest(); $viewer = $request->getUser(); - $table = new PhabricatorFactRaw(); - $conn_r = $table->establishConnection('r'); - $table_name = $table->getTableName(); - - $facts = queryfx_all( - $conn_r, - 'SELECT DISTINCT factType from %T', - $table_name); - - $specs = PhabricatorFactSpec::newSpecsForFactTypes( - PhabricatorFactEngine::loadAllEngines(), - ipull($facts, 'factType')); - - $options = array(); - foreach ($specs as $spec) { - if ($spec->getUnit() == PhabricatorFactSpec::UNIT_COUNT) { - $options[$spec->getType()] = $spec->getName(); - } - } - - if (!$options) { - return id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_NODATA) - ->setTitle(pht('No Chartable Facts')) - ->appendChild(phutil_tag( - 'p', - array(), - pht('There are no facts that can be plotted yet.'))); - } + $specs = PhabricatorFact::getAllFacts(); + $options = mpull($specs, 'getName', 'getKey'); $form = id(new AphrontFormView()) ->setUser($viewer) diff --git a/src/applications/fact/controller/PhabricatorFactObjectController.php b/src/applications/fact/controller/PhabricatorFactObjectController.php new file mode 100644 index 0000000000..bffc1ef157 --- /dev/null +++ b/src/applications/fact/controller/PhabricatorFactObjectController.php @@ -0,0 +1,323 @@ +getViewer(); + + $phid = $request->getURIData('phid'); + $object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withNames(array($phid)) + ->executeOne(); + if (!$object) { + return new Aphront404Response(); + } + + $engines = PhabricatorFactEngine::loadAllEngines(); + foreach ($engines as $key => $engine) { + $engine = id(clone $engine) + ->setViewer($viewer); + $engines[$key] = $engine; + + if (!$engine->supportsDatapointsForObject($object)) { + unset($engines[$key]); + } + } + + if (!$engines) { + return $this->newDialog() + ->setTitle(pht('No Engines')) + ->appendParagraph( + pht( + 'No fact engines support generating facts for this object.')) + ->addCancelButton($this->getApplicationURI()); + } + + $key_dimension = new PhabricatorFactKeyDimension(); + $object_phid = $object->getPHID(); + + $facts = array(); + $generated_datapoints = array(); + $timings = array(); + foreach ($engines as $key => $engine) { + $engine_facts = $engine->newFacts(); + $engine_facts = mpull($engine_facts, null, 'getKey'); + $facts[$key] = $engine_facts; + + $t_start = microtime(true); + $generated_datapoints[$key] = $engine->newDatapointsForObject($object); + $t_end = microtime(true); + + $timings[$key] = ($t_end - $t_start); + } + + $object_id = id(new PhabricatorFactObjectDimension()) + ->newDimensionID($object_phid, true); + + $stored_datapoints = id(new PhabricatorFactDatapointQuery()) + ->withFacts(array_mergev($facts)) + ->withObjectPHIDs(array($object_phid)) + ->needVectors(true) + ->execute(); + + $stored_groups = array(); + foreach ($stored_datapoints as $stored_datapoint) { + $stored_groups[$stored_datapoint['key']][] = $stored_datapoint; + } + + $stored_map = array(); + foreach ($engines as $key => $engine) { + $stored_map[$key] = array(); + foreach ($facts[$key] as $fact) { + $fact_datapoints = idx($stored_groups, $fact->getKey(), array()); + $fact_datapoints = igroup($fact_datapoints, 'vector'); + $stored_map[$key] += $fact_datapoints; + } + } + + $handle_phids = array(); + $handle_phids[] = $object->getPHID(); + foreach ($generated_datapoints as $key => $datapoint_set) { + foreach ($datapoint_set as $datapoint) { + $dimension_phid = $datapoint->getDimensionPHID(); + if ($dimension_phid !== null) { + $handle_phids[$dimension_phid] = $dimension_phid; + } + } + } + + foreach ($stored_map as $key => $stored_datapoints) { + foreach ($stored_datapoints as $vector_key => $datapoints) { + foreach ($datapoints as $datapoint) { + $dimension_phid = $datapoint['dimensionPHID']; + if ($dimension_phid !== null) { + $handle_phids[$dimension_phid] = $dimension_phid; + } + } + } + } + + $handles = $viewer->loadHandles($handle_phids); + + $dimension_map = id(new PhabricatorFactObjectDimension()) + ->newDimensionMap($handle_phids, true); + + $content = array(); + + $object_list = id(new PHUIPropertyListView()) + ->setViewer($viewer) + ->addProperty( + pht('Object'), + $handles[$object->getPHID()]->renderLink()); + + $total_cost = array_sum($timings); + $total_cost = pht('%sms', new PhutilNumber((int)(1000 * $total_cost))); + $object_list->addProperty(pht('Total Cost'), $total_cost); + + $object_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Fact Extraction Report')) + ->addPropertyList($object_list); + + $content[] = $object_box; + + $icon_fact = id(new PHUIIconView()) + ->setIcon('fa-line-chart green') + ->setTooltip(pht('Consistent Fact')); + + $icon_nodata = id(new PHUIIconView()) + ->setIcon('fa-question-circle-o violet') + ->setTooltip(pht('No Stored Datapoints')); + + $icon_new = id(new PHUIIconView()) + ->setIcon('fa-plus red') + ->setTooltip(pht('Not Stored')); + + $icon_surplus = id(new PHUIIconView()) + ->setIcon('fa-minus red') + ->setTooltip(pht('Not Generated')); + + foreach ($engines as $key => $engine) { + $rows = array(); + foreach ($generated_datapoints[$key] as $datapoint) { + $dimension_phid = $datapoint->getDimensionPHID(); + if ($dimension_phid !== null) { + $dimension = $handles[$datapoint->getDimensionPHID()]->renderLink(); + } else { + $dimension = null; + } + + $fact_key = $datapoint->getKey(); + + $fact = idx($facts[$key], $fact_key, null); + if ($fact) { + $fact_label = $fact->getName(); + } else { + $fact_label = $fact_key; + } + + $vector_key = $datapoint->newDatapointVector(); + if (isset($stored_map[$key][$vector_key])) { + unset($stored_map[$key][$vector_key]); + $icon = $icon_fact; + } else { + $icon = $icon_new; + } + + $rows[] = array( + $icon, + $fact_label, + $dimension, + $datapoint->getValue(), + phabricator_datetime($datapoint->getEpoch(), $viewer), + ); + } + + foreach ($stored_map[$key] as $vector_key => $datapoints) { + foreach ($datapoints as $datapoint) { + $dimension_phid = $datapoint['dimensionPHID']; + if ($dimension_phid !== null) { + $dimension = $handles[$dimension_phid]->renderLink(); + } else { + $dimension = null; + } + + $fact_key = $datapoint['key']; + $fact = idx($facts[$key], $fact_key, null); + if ($fact) { + $fact_label = $fact->getName(); + } else { + $fact_label = $fact_key; + } + + $rows[] = array( + $icon_surplus, + $fact_label, + $dimension, + $datapoint['value'], + phabricator_datetime($datapoint['epoch'], $viewer), + ); + } + } + + foreach ($facts[$key] as $fact) { + $has_any = id(new PhabricatorFactDatapointQuery()) + ->withFacts(array($fact)) + ->setLimit(1) + ->execute(); + if ($has_any) { + continue; + } + + if (!$has_any) { + $rows[] = array( + $icon_nodata, + $fact->getName(), + null, + null, + null, + ); + } + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + null, + pht('Fact'), + pht('Dimension'), + pht('Value'), + pht('Date'), + )) + ->setColumnClasses( + array( + '', + '', + '', + 'n wide right', + 'right', + )); + + $extraction_cost = $timings[$key]; + $extraction_cost = pht( + '%sms', + new PhutilNumber((int)(1000 * $extraction_cost))); + + $header = pht( + '%s (%s)', + get_class($engine), + $extraction_cost); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText($header) + ->setTable($table); + + $content[] = $box; + + if ($engine instanceof PhabricatorTransactionFactEngine) { + $groups = $engine->newTransactionGroupsForObject($object); + $groups = array_values($groups); + + $xaction_phids = array(); + foreach ($groups as $group_key => $xactions) { + foreach ($xactions as $xaction) { + $xaction_phids[] = $xaction->getAuthorPHID(); + } + } + $xaction_handles = $viewer->loadHandles($xaction_phids); + + $rows = array(); + foreach ($groups as $group_key => $xactions) { + foreach ($xactions as $xaction) { + $rows[] = array( + $group_key, + $xaction->getTransactionType(), + $xaction_handles[$xaction->getAuthorPHID()]->renderLink(), + phabricator_datetime($xaction->getDateCreated(), $viewer), + ); + } + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('Group'), + pht('Type'), + pht('Author'), + pht('Date'), + )) + ->setColumnClasses( + array( + null, + 'pri', + 'wide', + 'right', + )); + + $header = pht( + '%s (Transactions)', + get_class($engine)); + + $xaction_box = id(new PHUIObjectBoxView()) + ->setHeaderText($header) + ->setTable($table); + + $content[] = $xaction_box; + } + + } + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Chart')); + + $title = pht('Chart'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($content); + + } + +} diff --git a/src/applications/fact/daemon/PhabricatorFactDaemon.php b/src/applications/fact/daemon/PhabricatorFactDaemon.php index 384bb56df4..3fa6c6f0ff 100644 --- a/src/applications/fact/daemon/PhabricatorFactDaemon.php +++ b/src/applications/fact/daemon/PhabricatorFactDaemon.php @@ -4,8 +4,6 @@ final class PhabricatorFactDaemon extends PhabricatorDaemon { private $engines; - const RAW_FACT_BUFFER_LIMIT = 128; - protected function run() { $this->setEngines(PhabricatorFactEngine::loadAllEngines()); while (!$this->shouldExit()) { @@ -15,7 +13,6 @@ final class PhabricatorFactDaemon extends PhabricatorDaemon { foreach ($iterators as $iterator_name => $iterator) { $this->processIteratorWithCursor($iterator_name, $iterator); } - $this->processAggregates(); $this->log(pht('Zzz...')); $this->sleep(60 * 5); @@ -65,6 +62,11 @@ final class PhabricatorFactDaemon extends PhabricatorDaemon { public function setEngines(array $engines) { assert_instances_of($engines, 'PhabricatorFactEngine'); + $viewer = PhabricatorUser::getOmnipotentUser(); + foreach ($engines as $engine) { + $engine->setViewer($viewer); + } + $this->engines = $engines; return $this; } @@ -72,59 +74,49 @@ final class PhabricatorFactDaemon extends PhabricatorDaemon { public function processIterator($iterator) { $result = null; - $raw_facts = array(); + $datapoints = array(); + $count = 0; foreach ($iterator as $key => $object) { $phid = $object->getPHID(); $this->log(pht('Processing %s...', $phid)); - $raw_facts[$phid] = $this->computeRawFacts($object); - if (count($raw_facts) > self::RAW_FACT_BUFFER_LIMIT) { - $this->updateRawFacts($raw_facts); - $raw_facts = array(); + $object_datapoints = $this->newDatapoints($object); + $count += count($object_datapoints); + + $datapoints[$phid] = $object_datapoints; + + if ($count > 1024) { + $this->updateDatapoints($datapoints); + $datapoints = array(); + $count = 0; } + $result = $key; } - if ($raw_facts) { - $this->updateRawFacts($raw_facts); - $raw_facts = array(); + if ($count) { + $this->updateDatapoints($datapoints); + $datapoints = array(); + $count = 0; } return $result; } - public function processAggregates() { - $this->log(pht('Processing aggregates.')); - - $facts = $this->computeAggregateFacts(); - $this->updateAggregateFacts($facts); - } - - private function computeAggregateFacts() { + private function newDatapoints(PhabricatorLiskDAO $object) { $facts = array(); foreach ($this->engines as $engine) { - if (!$engine->shouldComputeAggregateFacts()) { + if (!$engine->supportsDatapointsForObject($object)) { continue; } - $facts[] = $engine->computeAggregateFacts(); - } - return array_mergev($facts); - } - - private function computeRawFacts(PhabricatorLiskDAO $object) { - $facts = array(); - foreach ($this->engines as $engine) { - if (!$engine->shouldComputeRawFactsForObject($object)) { - continue; - } - $facts[] = $engine->computeRawFactsForObject($object); + $facts[] = $engine->newDatapointsForObject($object); } return array_mergev($facts); } - private function updateRawFacts(array $map) { + private function updateDatapoints(array $map) { foreach ($map as $phid => $facts) { - assert_instances_of($facts, 'PhabricatorFactRaw'); + assert_instances_of($facts, 'PhabricatorFactIntDatapoint'); } $phids = array_keys($map); @@ -132,76 +124,78 @@ final class PhabricatorFactDaemon extends PhabricatorDaemon { return; } - $table = new PhabricatorFactRaw(); + $fact_keys = array(); + $objects = array(); + foreach ($map as $phid => $facts) { + foreach ($facts as $fact) { + $fact_keys[$fact->getKey()] = true; + + $object_phid = $fact->getObjectPHID(); + $objects[$object_phid] = $object_phid; + + $dimension_phid = $fact->getDimensionPHID(); + if ($dimension_phid !== null) { + $objects[$dimension_phid] = $dimension_phid; + } + } + } + + $key_map = id(new PhabricatorFactKeyDimension()) + ->newDimensionMap(array_keys($fact_keys), true); + $object_map = id(new PhabricatorFactObjectDimension()) + ->newDimensionMap(array_keys($objects), true); + + $table = new PhabricatorFactIntDatapoint(); $conn = $table->establishConnection('w'); $table_name = $table->getTableName(); $sql = array(); foreach ($map as $phid => $facts) { foreach ($facts as $fact) { + $key_id = $key_map[$fact->getKey()]; + $object_id = $object_map[$fact->getObjectPHID()]; + + $dimension_phid = $fact->getDimensionPHID(); + if ($dimension_phid !== null) { + $dimension_id = $object_map[$dimension_phid]; + } else { + $dimension_id = null; + } + $sql[] = qsprintf( $conn, - '(%s, %s, %s, %d, %d, %d)', - $fact->getFactType(), - $fact->getObjectPHID(), - $fact->getObjectA(), - $fact->getValueX(), - $fact->getValueY(), + '(%d, %d, %nd, %d, %d)', + $key_id, + $object_id, + $dimension_id, + $fact->getValue(), $fact->getEpoch()); } } + $rebuilt_ids = array_select_keys($object_map, $phids); + $table->openTransaction(); queryfx( $conn, - 'DELETE FROM %T WHERE objectPHID IN (%Ls)', + 'DELETE FROM %T WHERE objectID IN (%Ld)', $table_name, - $phids); + $rebuilt_ids); if ($sql) { - foreach (array_chunk($sql, 256) as $chunk) { + foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn, 'INSERT INTO %T - (factType, objectPHID, objectA, valueX, valueY, epoch) + (keyID, objectID, dimensionID, value, epoch) VALUES %Q', $table_name, - implode(', ', $chunk)); + $chunk); } } $table->saveTransaction(); } - private function updateAggregateFacts(array $facts) { - if (!$facts) { - return; - } - - $table = new PhabricatorFactAggregate(); - $conn = $table->establishConnection('w'); - $table_name = $table->getTableName(); - - $sql = array(); - foreach ($facts as $fact) { - $sql[] = qsprintf( - $conn, - '(%s, %s, %d)', - $fact->getFactType(), - $fact->getObjectPHID(), - $fact->getValueX()); - } - - foreach (array_chunk($sql, 256) as $chunk) { - queryfx( - $conn, - 'INSERT INTO %T (factType, objectPHID, valueX) VALUES %Q - ON DUPLICATE KEY UPDATE valueX = VALUES(valueX)', - $table_name, - implode(', ', $chunk)); - } - - } - } diff --git a/src/applications/fact/engine/PhabricatorFactCountEngine.php b/src/applications/fact/engine/PhabricatorFactCountEngine.php deleted file mode 100644 index f24068646d..0000000000 --- a/src/applications/fact/engine/PhabricatorFactCountEngine.php +++ /dev/null @@ -1,86 +0,0 @@ -setName($name) - ->setUnit(PhabricatorFactSimpleSpec::UNIT_COUNT); - } - - if (!strncmp($type, 'N:', 2)) { - if ($type == 'N:*') { - $name = pht('Objects'); - } else { - $name = pht('Objects of type %s', substr($type, 2)); - } - $results[] = id(new PhabricatorFactSimpleSpec($type)) - ->setName($name) - ->setUnit(PhabricatorFactSimpleSpec::UNIT_COUNT); - } - - } - return $results; - } - - public function shouldComputeRawFactsForObject(PhabricatorLiskDAO $object) { - return true; - } - - public function computeRawFactsForObject(PhabricatorLiskDAO $object) { - $facts = array(); - - $phid = $object->getPHID(); - $type = phid_get_type($phid); - - foreach (array('N:*', 'N:'.$type) as $fact_type) { - $facts[] = id(new PhabricatorFactRaw()) - ->setFactType($fact_type) - ->setObjectPHID($phid) - ->setValueX(1) - ->setEpoch($object->getDateCreated()); - } - - return $facts; - } - - public function shouldComputeAggregateFacts() { - return true; - } - - public function computeAggregateFacts() { - $table = new PhabricatorFactRaw(); - $table_name = $table->getTableName(); - $conn = $table->establishConnection('r'); - - $counts = queryfx_all( - $conn, - 'SELECT factType, SUM(valueX) N FROM %T WHERE factType LIKE %> - GROUP BY factType', - $table_name, - 'N:'); - - $facts = array(); - foreach ($counts as $count) { - $facts[] = id(new PhabricatorFactAggregate()) - ->setFactType('+'.$count['factType']) - ->setValueX($count['N']); - } - - return $facts; - } - - -} diff --git a/src/applications/fact/engine/PhabricatorFactEngine.php b/src/applications/fact/engine/PhabricatorFactEngine.php index a87cabf56d..ba2cf966e5 100644 --- a/src/applications/fact/engine/PhabricatorFactEngine.php +++ b/src/applications/fact/engine/PhabricatorFactEngine.php @@ -2,30 +2,51 @@ abstract class PhabricatorFactEngine extends Phobject { + private $factMap; + private $viewer; + final public static function loadAllEngines() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->execute(); } - public function getFactSpecs(array $fact_types) { - return array(); + abstract public function newFacts(); + + abstract public function supportsDatapointsForObject( + PhabricatorLiskDAO $object); + + abstract public function newDatapointsForObject(PhabricatorLiskDAO $object); + + final protected function getFact($key) { + if ($this->factMap === null) { + $facts = $this->newFacts(); + $facts = mpull($facts, null, 'getKey'); + $this->factMap = $facts; + } + + if (!isset($this->factMap[$key])) { + throw new Exception( + pht( + 'Unknown fact ("%s") for engine "%s".', + $key, + get_class($this))); + } + + return $this->factMap[$key]; } - public function shouldComputeRawFactsForObject(PhabricatorLiskDAO $object) { - return false; + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; } - public function computeRawFactsForObject(PhabricatorLiskDAO $object) { - return array(); - } + public function getViewer() { + if (!$this->viewer) { + throw new PhutilInvalidStateException('setViewer'); + } - public function shouldComputeAggregateFacts() { - return false; - } - - public function computeAggregateFacts() { - return array(); + return $this->viewer; } } diff --git a/src/applications/fact/engine/PhabricatorFactLastUpdatedEngine.php b/src/applications/fact/engine/PhabricatorFactLastUpdatedEngine.php deleted file mode 100644 index 5ea99e6232..0000000000 --- a/src/applications/fact/engine/PhabricatorFactLastUpdatedEngine.php +++ /dev/null @@ -1,34 +0,0 @@ -setName(pht('Facts Last Updated')) - ->setUnit(PhabricatorFactSimpleSpec::UNIT_EPOCH); - } - } - return $results; - } - - public function shouldComputeAggregateFacts() { - return true; - } - - public function computeAggregateFacts() { - $facts = array(); - - $facts[] = id(new PhabricatorFactAggregate()) - ->setFactType('updated') - ->setValueX(time()); - - return $facts; - } - -} diff --git a/src/applications/fact/engine/PhabricatorManiphestTaskFactEngine.php b/src/applications/fact/engine/PhabricatorManiphestTaskFactEngine.php new file mode 100644 index 0000000000..dffceedb77 --- /dev/null +++ b/src/applications/fact/engine/PhabricatorManiphestTaskFactEngine.php @@ -0,0 +1,411 @@ +setKey('tasks.count.create'), + + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.create'), + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.status'), + + id(new PhabricatorCountFact()) + ->setKey('tasks.count.create.project'), + id(new PhabricatorCountFact()) + ->setKey('tasks.count.assign.project'), + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.create.project'), + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.status.project'), + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.assign.project'), + + id(new PhabricatorCountFact()) + ->setKey('tasks.count.create.owner'), + id(new PhabricatorCountFact()) + ->setKey('tasks.count.assign.owner'), + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.create.owner'), + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.status.owner'), + id(new PhabricatorCountFact()) + ->setKey('tasks.open-count.assign.owner'), + + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.create'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.score'), + + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.create'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.status'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.score'), + + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.create.project'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.assign.project'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.score.project'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.create.project'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.status.project'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.score.project'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.assign.project'), + + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.create.owner'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.assign.owner'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.points.score.owner'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.create.owner'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.status.owner'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.score.owner'), + id(new PhabricatorPointsFact()) + ->setKey('tasks.open-points.assign.owner'), + ); + } + + public function supportsDatapointsForObject(PhabricatorLiskDAO $object) { + return ($object instanceof ManiphestTask); + } + + public function newDatapointsForObject(PhabricatorLiskDAO $object) { + $xaction_groups = $this->newTransactionGroupsForObject($object); + + $old_open = false; + $old_points = 0; + $old_owner = null; + $project_map = array(); + $object_phid = $object->getPHID(); + $is_create = true; + + $specs = array(); + $datapoints = array(); + foreach ($xaction_groups as $xaction_group) { + $add_projects = array(); + $rem_projects = array(); + + $new_open = $old_open; + $new_points = $old_points; + $new_owner = $old_owner; + + if ($is_create) { + // Assume tasks start open. + // TODO: This might be a questionable assumption? + $new_open = true; + } + + $group_epoch = last($xaction_group)->getDateCreated(); + foreach ($xaction_group as $xaction) { + $old_value = $xaction->getOldValue(); + $new_value = $xaction->getNewValue(); + switch ($xaction->getTransactionType()) { + case ManiphestTaskStatusTransaction::TRANSACTIONTYPE: + $new_open = !ManiphestTaskStatus::isClosedStatus($new_value); + break; + case ManiphestTaskMergedIntoTransaction::TRANSACTIONTYPE: + // When a task is merged into another task, it is changed to a + // closed status without generating a separate status transaction. + $new_open = false; + break; + case ManiphestTaskPointsTransaction::TRANSACTIONTYPE: + $new_points = (int)$xaction->getNewValue(); + break; + case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE: + $new_owner = $xaction->getNewValue(); + break; + case PhabricatorTransactions::TYPE_EDGE: + $edge_type = $xaction->getMetadataValue('edge:type'); + switch ($edge_type) { + case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST: + $record = PhabricatorEdgeChangeRecord::newFromTransaction( + $xaction); + $add_projects += array_fuse($record->getAddedPHIDs()); + $rem_projects += array_fuse($record->getRemovedPHIDs()); + break; + } + break; + } + } + + // If a project was both added and removed, moot it. + $mix_projects = array_intersect_key($add_projects, $rem_projects); + $add_projects = array_diff_key($add_projects, $mix_projects); + $rem_projects = array_diff_key($rem_projects, $mix_projects); + + $project_sets = array( + array( + 'phids' => $rem_projects, + 'scale' => -1, + ), + array( + 'phids' => $add_projects, + 'scale' => 1, + ), + ); + + if ($is_create) { + $action = 'create'; + $action_points = $new_points; + $include_open = $new_open; + } else { + $action = 'assign'; + $action_points = $old_points; + $include_open = $old_open; + } + + foreach ($project_sets as $project_set) { + $scale = $project_set['scale']; + foreach ($project_set['phids'] as $project_phid) { + if ($include_open) { + $specs[] = array( + "tasks.open-count.{$action}.project", + 1 * $scale, + $project_phid, + ); + + $specs[] = array( + "tasks.open-points.{$action}.project", + $action_points * $scale, + $project_phid, + ); + } + + $specs[] = array( + "tasks.count.{$action}.project", + 1 * $scale, + $project_phid, + ); + + $specs[] = array( + "tasks.points.{$action}.project", + $action_points * $scale, + $project_phid, + ); + + if ($scale < 0) { + unset($project_map[$project_phid]); + } else { + $project_map[$project_phid] = $project_phid; + } + } + } + + if ($new_owner !== $old_owner) { + $owner_sets = array( + array( + 'phid' => $old_owner, + 'scale' => -1, + ), + array( + 'phid' => $new_owner, + 'scale' => 1, + ), + ); + + foreach ($owner_sets as $owner_set) { + $owner_phid = $owner_set['phid']; + if ($owner_phid === null) { + continue; + } + + $scale = $owner_set['scale']; + + if ($old_open != $new_open) { + $specs[] = array( + "tasks.open-count.{$action}.owner", + 1 * $scale, + $owner_phid, + ); + + $specs[] = array( + "tasks.open-points.{$action}.owner", + $action_points * $scale, + $owner_phid, + ); + } + + $specs[] = array( + "tasks.count.{$action}.owner", + 1 * $scale, + $owner_phid, + ); + + if ($action_points) { + $specs[] = array( + "tasks.points.{$action}.owner", + $action_points * $scale, + $owner_phid, + ); + } + } + } + + if ($is_create) { + $specs[] = array( + 'tasks.count.create', + 1, + ); + + $specs[] = array( + 'tasks.points.create', + $new_points, + ); + + if ($new_open) { + $specs[] = array( + 'tasks.open-count.create', + 1, + ); + $specs[] = array( + 'tasks.open-points.create', + $new_points, + ); + } + } else if ($new_open !== $old_open) { + if ($new_open) { + $scale = 1; + } else { + $scale = -1; + } + + $specs[] = array( + 'tasks.open-count.status', + 1 * $scale, + ); + + $specs[] = array( + 'tasks.open-points.status', + $action_points * $scale, + ); + + if ($new_owner !== null) { + $specs[] = array( + 'tasks.open-count.status.owner', + 1 * $scale, + $new_owner, + ); + $specs[] = array( + 'tasks.open-points.status.owner', + $action_points * $scale, + $new_owner, + ); + } + + foreach ($project_map as $project_phid) { + $specs[] = array( + 'tasks.open-count.status.project', + 1 * $scale, + $project_phid, + ); + $specs[] = array( + 'tasks.open-points.status.project', + $action_points * $scale, + $project_phid, + ); + } + } + + // The "score" facts only apply to rescoring tasks which already + // exist, so we skip them if the task is being created. + if (($new_points !== $old_points) && !$is_create) { + $delta = ($new_points - $old_points); + + $specs[] = array( + 'tasks.points.score', + $delta, + ); + + foreach ($project_map as $project_phid) { + $specs[] = array( + 'tasks.points.score.project', + $delta, + $project_phid, + ); + + if ($old_open && $new_open) { + $specs[] = array( + 'tasks.open-points.score.project', + $delta, + $project_phid, + ); + } + } + + if ($new_owner !== null) { + $specs[] = array( + 'tasks.points.score.owner', + $delta, + $new_owner, + ); + + if ($old_open && $new_open) { + $specs[] = array( + 'tasks.open-points.score.owner', + $delta, + $new_owner, + ); + } + } + + if ($old_open && $new_open) { + $specs[] = array( + 'tasks.open-points.score', + $delta, + ); + } + } + + $old_points = $new_points; + $old_open = $new_open; + $old_owner = $new_owner; + + foreach ($specs as $spec) { + $spec_key = $spec[0]; + $spec_value = $spec[1]; + + // Don't write any facts with a value of 0. The "count" facts never + // have a value of 0, and the "points" facts aren't meaningful if + // they have a value of 0. + if ($spec_value == 0) { + continue; + } + + $datapoint = $this->getFact($spec_key) + ->newDatapoint(); + + $datapoint + ->setObjectPHID($object_phid) + ->setValue($spec_value) + ->setEpoch($group_epoch); + + if (isset($spec[2])) { + $datapoint->setDimensionPHID($spec[2]); + } + + $datapoints[] = $datapoint; + } + + $specs = array(); + $is_create = false; + } + + return $datapoints; + } + + +} diff --git a/src/applications/fact/engine/PhabricatorTransactionFactEngine.php b/src/applications/fact/engine/PhabricatorTransactionFactEngine.php new file mode 100644 index 0000000000..20fb7a6f66 --- /dev/null +++ b/src/applications/fact/engine/PhabricatorTransactionFactEngine.php @@ -0,0 +1,84 @@ +getViewer(); + + $xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject( + $object); + $xactions = $xaction_query + ->setViewer($viewer) + ->withObjectPHIDs(array($object->getPHID())) + ->execute(); + + $xactions = msortv($xactions, 'newChronologicalSortVector'); + + return $this->groupTransactions($xactions); + } + + protected function groupTransactions(array $xactions) { + // These grouping rules are generally much looser than the display grouping + // rules. As long as the same user is editing the task and they don't leave + // it alone for a particularly long time, we'll group things together. + + $breaks = array(); + + $touch_window = phutil_units('15 minutes in seconds'); + $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; + + $last_actor = null; + $last_epoch = null; + + foreach ($xactions as $key => $xaction) { + $this_actor = $xaction->getAuthorPHID(); + if (phid_get_type($this_actor) != $user_type) { + $this_actor = null; + } + + if ($this_actor && $last_actor && ($this_actor != $last_actor)) { + $breaks[$key] = true; + } + + // If too much time passed between changes, group them separately. + $this_epoch = $xaction->getDateCreated(); + if ($last_epoch) { + if (($this_epoch - $last_epoch) > $touch_window) { + $breaks[$key] = true; + } + } + + // The clock gets reset every time the same real user touches the + // task, but does not reset if an automated actor touches things. + if (!$last_actor || ($this_actor == $last_actor)) { + $last_epoch = $this_epoch; + } + + if ($this_actor && ($last_actor != $this_actor)) { + $last_actor = $this_actor; + $last_epoch = $this_epoch; + } + } + + $groups = array(); + $group = array(); + foreach ($xactions as $key => $xaction) { + if (isset($breaks[$key])) { + if ($group) { + $groups[] = $group; + $group = array(); + } + } + + $group[] = $xaction; + } + + if ($group) { + $groups[] = $group; + } + + return $groups; + } + +} diff --git a/src/applications/fact/fact/PhabricatorCountFact.php b/src/applications/fact/fact/PhabricatorCountFact.php new file mode 100644 index 0000000000..41e6fd4de3 --- /dev/null +++ b/src/applications/fact/fact/PhabricatorCountFact.php @@ -0,0 +1,9 @@ +newFacts(); + $facts = mpull($facts, null, 'getKey'); + $map += $facts; + } + + return $map; + } + + final public function setKey($key) { + $this->key = $key; + return $this; + } + + final public function getKey() { + return $this->key; + } + + final public function getName() { + return pht('Fact "%s"', $this->getKey()); + } + + final public function newDatapoint() { + return $this->newTemplateDatapoint() + ->setKey($this->getKey()); + } + + abstract protected function newTemplateDatapoint(); + +} diff --git a/src/applications/fact/fact/PhabricatorPointsFact.php b/src/applications/fact/fact/PhabricatorPointsFact.php new file mode 100644 index 0000000000..a80f45d132 --- /dev/null +++ b/src/applications/fact/fact/PhabricatorPointsFact.php @@ -0,0 +1,9 @@ +getArg('skip-aggregates')) { - $daemon->processAggregates(); - } - return 0; } diff --git a/src/applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php index 8a2e5f1ed8..7a4997ab35 100644 --- a/src/applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php +++ b/src/applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php @@ -23,8 +23,13 @@ final class PhabricatorFactManagementDestroyWorkflow } $tables = array(); - $tables[] = new PhabricatorFactRaw(); - $tables[] = new PhabricatorFactAggregate(); + $tables[] = new PhabricatorFactCursor(); + + $tables[] = new PhabricatorFactIntDatapoint(); + + $tables[] = new PhabricatorFactObjectDimension(); + $tables[] = new PhabricatorFactKeyDimension(); + foreach ($tables as $table) { $conn = $table->establishConnection('w'); $name = $table->getTableName(); diff --git a/src/applications/fact/management/PhabricatorFactManagementStatusWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementStatusWorkflow.php deleted file mode 100644 index 604a40b6ee..0000000000 --- a/src/applications/fact/management/PhabricatorFactManagementStatusWorkflow.php +++ /dev/null @@ -1,47 +0,0 @@ -setName('status') - ->setSynopsis(pht('Show status of fact data.')) - ->setArguments(array()); - } - - public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); - - $map = array( - 'raw' => new PhabricatorFactRaw(), - 'agg' => new PhabricatorFactAggregate(), - ); - - foreach ($map as $type => $table) { - $conn = $table->establishConnection('r'); - $name = $table->getTableName(); - - $row = queryfx_one( - $conn, - 'SELECT COUNT(*) N FROM %T', - $name); - - $n = $row['N']; - - switch ($type) { - case 'raw': - $desc = pht('There are %d raw fact(s) in storage.', $n); - break; - case 'agg': - $desc = pht('There are %d aggregate fact(s) in storage.', $n); - break; - } - - $console->writeOut("%s\n", $desc); - } - - return 0; - } - -} diff --git a/src/applications/fact/query/PhabricatorFactDatapointQuery.php b/src/applications/fact/query/PhabricatorFactDatapointQuery.php new file mode 100644 index 0000000000..fee7c86d12 --- /dev/null +++ b/src/applications/fact/query/PhabricatorFactDatapointQuery.php @@ -0,0 +1,181 @@ +facts = $facts; + return $this; + } + + public function withObjectPHIDs(array $object_phids) { + $this->objectPHIDs = $object_phids; + return $this; + } + + public function setLimit($limit) { + $this->limit = $limit; + return $this; + } + + public function needVectors($need) { + $this->needVectors = $need; + return $this; + } + + public function execute() { + $facts = mpull($this->facts, null, 'getKey'); + if (!$facts) { + throw new Exception(pht('Executing a fact query requires facts.')); + } + + $table_map = array(); + foreach ($facts as $fact) { + $datapoint = $fact->newDatapoint(); + $table = $datapoint->getTableName(); + + if (!isset($table_map[$table])) { + $table_map[$table] = array( + 'table' => $datapoint, + 'facts' => array(), + ); + } + + $table_map[$table]['facts'][] = $fact; + } + + $rows = array(); + foreach ($table_map as $spec) { + $rows[] = $this->executeWithTable($spec); + } + $rows = array_mergev($rows); + + $key_unmap = array_flip($this->keyMap); + $dimension_unmap = array_flip($this->dimensionMap); + + $groups = array(); + $need_phids = array(); + foreach ($rows as $row) { + $groups[$row['keyID']][] = $row; + + $object_id = $row['objectID']; + if (!isset($dimension_unmap[$object_id])) { + $need_phids[$object_id] = $object_id; + } + + $dimension_id = $row['dimensionID']; + if ($dimension_id && !isset($dimension_unmap[$dimension_id])) { + $need_phids[$dimension_id] = $dimension_id; + } + } + + $dimension_unmap += id(new PhabricatorFactObjectDimension()) + ->newDimensionUnmap($need_phids); + + $results = array(); + foreach ($groups as $key_id => $rows) { + $key = $key_unmap[$key_id]; + $fact = $facts[$key]; + $datapoint = $fact->newDatapoint(); + foreach ($rows as $row) { + $dimension_id = $row['dimensionID']; + if ($dimension_id) { + if (!isset($dimension_unmap[$dimension_id])) { + continue; + } else { + $dimension_phid = $dimension_unmap[$dimension_id]; + } + } else { + $dimension_phid = null; + } + + $object_id = $row['objectID']; + if (!isset($dimension_unmap[$object_id])) { + continue; + } else { + $object_phid = $dimension_unmap[$object_id]; + } + + $result = array( + 'key' => $key, + 'objectPHID' => $object_phid, + 'dimensionPHID' => $dimension_phid, + 'value' => (int)$row['value'], + 'epoch' => $row['epoch'], + ); + + if ($this->needVectors) { + $result['vector'] = $datapoint->newRawVector($result); + } + + $results[] = $result; + } + } + + return $results; + } + + private function executeWithTable(array $spec) { + $table = $spec['table']; + $facts = $spec['facts']; + $conn = $table->establishConnection('r'); + + $fact_keys = mpull($facts, 'getKey'); + $this->keyMap = id(new PhabricatorFactKeyDimension()) + ->newDimensionMap($fact_keys); + + if (!$this->keyMap) { + return array(); + } + + $where = array(); + + $where[] = qsprintf( + $conn, + 'keyID IN (%Ld)', + $this->keyMap); + + if ($this->objectPHIDs) { + $object_map = id(new PhabricatorFactObjectDimension()) + ->newDimensionMap($this->objectPHIDs); + if (!$object_map) { + return array(); + } + + $this->dimensionMap = $object_map; + + $where[] = qsprintf( + $conn, + 'objectID IN (%Ld)', + $this->dimensionMap); + } + + $where = '('.implode(') AND (', $where).')'; + + if ($this->limit) { + $limit = qsprintf( + $conn, + 'LIMIT %d', + $this->limit); + } else { + $limit = ''; + } + + return queryfx_all( + $conn, + 'SELECT keyID, objectID, dimensionID, value, epoch + FROM %T WHERE %Q %Q', + $table->getTableName(), + $where, + $limit); + } + +} diff --git a/src/applications/fact/spec/PhabricatorFactSimpleSpec.php b/src/applications/fact/spec/PhabricatorFactSimpleSpec.php deleted file mode 100644 index 350b6367f1..0000000000 --- a/src/applications/fact/spec/PhabricatorFactSimpleSpec.php +++ /dev/null @@ -1,38 +0,0 @@ -type = $type; - } - - public function getType() { - return $this->type; - } - - public function setUnit($unit) { - $this->unit = $unit; - return $this; - } - - public function getUnit() { - return $this->unit; - } - - public function setName($name) { - $this->name = $name; - return $this; - } - - public function getName() { - if ($this->name !== null) { - return $this->name; - } - return parent::getName(); - } - -} diff --git a/src/applications/fact/spec/PhabricatorFactSpec.php b/src/applications/fact/spec/PhabricatorFactSpec.php deleted file mode 100644 index 47fcc01d8b..0000000000 --- a/src/applications/fact/spec/PhabricatorFactSpec.php +++ /dev/null @@ -1,53 +0,0 @@ -getFactSpecs($fact_types); - $specs = mpull($specs, null, 'getType'); - $map += $specs; - } - - foreach ($fact_types as $type) { - if (empty($map[$type])) { - $map[$type] = new PhabricatorFactSimpleSpec($type); - } - } - - return $map; - } - - abstract public function getType(); - - public function getUnit() { - return null; - } - - public function getName() { - return pht( - 'Fact (%s)', - $this->getType()); - } - - public function formatValueForDisplay(PhabricatorUser $user, $value) { - $unit = $this->getUnit(); - switch ($unit) { - case self::UNIT_COUNT: - return number_format($value); - case self::UNIT_EPOCH: - return phabricator_datetime($value, $user); - default: - return $value; - } - } - -} diff --git a/src/applications/fact/storage/PhabricatorFactDimension.php b/src/applications/fact/storage/PhabricatorFactDimension.php new file mode 100644 index 0000000000..9c05121c9c --- /dev/null +++ b/src/applications/fact/storage/PhabricatorFactDimension.php @@ -0,0 +1,110 @@ +newDimensionMap(array($key), $create); + return idx($map, $key); + } + + final public function newDimensionUnmap(array $ids) { + if (!$ids) { + return array(); + } + + $conn = $this->establishConnection('r'); + $column = $this->getDimensionColumnName(); + + $rows = queryfx_all( + $conn, + 'SELECT id, %C FROM %T WHERE id IN (%Ld)', + $column, + $this->getTableName(), + $ids); + $rows = ipull($rows, $column, 'id'); + + return $rows; + } + + final public function newDimensionMap(array $keys, $create = false) { + if (!$keys) { + return array(); + } + + $conn = $this->establishConnection('r'); + $column = $this->getDimensionColumnName(); + + $rows = queryfx_all( + $conn, + 'SELECT id, %C FROM %T WHERE %C IN (%Ls)', + $column, + $this->getTableName(), + $column, + $keys); + $rows = ipull($rows, 'id', $column); + + $map = array(); + $need = array(); + foreach ($keys as $key) { + if (isset($rows[$key])) { + $map[$key] = (int)$rows[$key]; + } else { + $need[] = $key; + } + } + + if (!$need) { + return $map; + } + + if (!$create) { + return $map; + } + + $sql = array(); + foreach ($need as $key) { + $sql[] = qsprintf( + $conn, + '(%s)', + $key); + } + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { + queryfx( + $conn, + 'INSERT IGNORE INTO %T (%C) VALUES %Q', + $this->getTableName(), + $column, + $chunk); + } + unset($unguarded); + + $rows = queryfx_all( + $conn, + 'SELECT id, %C FROM %T WHERE %C IN (%Ls)', + $column, + $this->getTableName(), + $column, + $need); + $rows = ipull($rows, 'id', $column); + + foreach ($need as $key) { + if (isset($rows[$key])) { + $map[$key] = (int)$rows[$key]; + } else { + throw new Exception( + pht( + 'Failed to load or generate dimension ID ("%s") for dimension '. + 'key "%s".', + get_class($this), + $key)); + } + } + + return $map; + } + +} diff --git a/src/applications/fact/storage/PhabricatorFactIntDatapoint.php b/src/applications/fact/storage/PhabricatorFactIntDatapoint.php new file mode 100644 index 0000000000..87a7c68adf --- /dev/null +++ b/src/applications/fact/storage/PhabricatorFactIntDatapoint.php @@ -0,0 +1,87 @@ + false, + self::CONFIG_COLUMN_SCHEMA => array( + 'id' => 'auto64', + 'dimensionID' => 'id?', + 'value' => 'sint64', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_dimension' => array( + 'columns' => array('keyID', 'dimensionID'), + ), + 'key_object' => array( + 'columns' => array('objectID'), + ), + ), + ) + parent::getConfiguration(); + } + + public function setKey($key) { + $this->key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setObjectPHID($object_phid) { + $this->objectPHID = $object_phid; + return $this; + } + + public function getObjectPHID() { + return $this->objectPHID; + } + + public function setDimensionPHID($dimension_phid) { + $this->dimensionPHID = $dimension_phid; + return $this; + } + + public function getDimensionPHID() { + return $this->dimensionPHID; + } + + public function newDatapointVector() { + return $this->formatVector( + array( + $this->key, + $this->objectPHID, + $this->dimensionPHID, + $this->value, + $this->epoch, + )); + } + + public function newRawVector(array $spec) { + return $this->formatVector( + array( + $spec['key'], + $spec['objectPHID'], + $spec['dimensionPHID'], + $spec['value'], + $spec['epoch'], + )); + } + + private function formatVector(array $vector) { + return implode(':', $vector); + } + +} diff --git a/src/applications/fact/storage/PhabricatorFactKeyDimension.php b/src/applications/fact/storage/PhabricatorFactKeyDimension.php new file mode 100644 index 0000000000..b58ba94400 --- /dev/null +++ b/src/applications/fact/storage/PhabricatorFactKeyDimension.php @@ -0,0 +1,27 @@ + false, + self::CONFIG_COLUMN_SCHEMA => array( + 'factKey' => 'text64', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_factkey' => array( + 'columns' => array('factKey'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + protected function getDimensionColumnName() { + return 'factKey'; + } + +} diff --git a/src/applications/fact/storage/PhabricatorFactObjectDimension.php b/src/applications/fact/storage/PhabricatorFactObjectDimension.php new file mode 100644 index 0000000000..e7319724a4 --- /dev/null +++ b/src/applications/fact/storage/PhabricatorFactObjectDimension.php @@ -0,0 +1,25 @@ + false, + self::CONFIG_COLUMN_SCHEMA => array(), + self::CONFIG_KEY_SCHEMA => array( + 'key_object' => array( + 'columns' => array('objectPHID'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + protected function getDimensionColumnName() { + return 'objectPHID'; + } + +} diff --git a/src/applications/files/markup/PhabricatorImageRemarkupRule.php b/src/applications/files/markup/PhabricatorImageRemarkupRule.php index 9e91bdc096..36089dc57e 100644 --- a/src/applications/files/markup/PhabricatorImageRemarkupRule.php +++ b/src/applications/files/markup/PhabricatorImageRemarkupRule.php @@ -20,7 +20,6 @@ final class PhabricatorImageRemarkupRule extends PhutilRemarkupRule { $defaults = array( 'uri' => null, 'alt' => null, - 'href' => null, 'width' => null, 'height' => null, ); @@ -45,10 +44,6 @@ final class PhabricatorImageRemarkupRule extends PhutilRemarkupRule { $args += $defaults; - if ($args['href'] && !PhabricatorEnv::isValidURIForLink($args['href'])) { - $args['href'] = null; - } - if ($args['uri']) { $src_uri = id(new PhutilURI('/file/imageproxy/')) ->setQueryParam('uri', (string)$args['uri']); @@ -57,10 +52,9 @@ final class PhabricatorImageRemarkupRule extends PhutilRemarkupRule { array( 'src' => $src_uri, 'alt' => $args['alt'], - 'href' => $args['href'], 'width' => $args['width'], 'height' => $args['height'], - )); + )); return $this->getEngine()->storeText($img); } else { return $matches[0]; diff --git a/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php b/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php index 345621f0f5..93f7564033 100644 --- a/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php +++ b/src/applications/harbormaster/artifact/HarbormasterURIArtifact.php @@ -81,6 +81,7 @@ final class HarbormasterURIArtifact extends HarbormasterArtifact { array( 'href' => $uri, 'target' => '_blank', + 'rel' => 'noreferrer', ), $name); } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php index 9dfd6a3eb6..5326e10639 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -1302,6 +1302,11 @@ final class PhabricatorMetaMTAMail $headers[] = array('Thread-Topic', $related_phid); } + $headers[] = array('X-Phabricator-Mail-ID', $this->getID()); + + $unique = Filesystem::readRandomCharacters(16); + $headers[] = array('X-Phabricator-Send-Attempt', $unique); + return $headers; } @@ -1356,6 +1361,8 @@ final class PhabricatorMetaMTAMail 'X-Phabricator-Sent-This-Message', 'X-Phabricator-Must-Encrypt', + 'X-Phabricator-Mail-ID', + 'X-Phabricator-Send-Attempt', ); // NOTE: The major header we want to drop is "X-Phabricator-Mail-Tags". diff --git a/src/applications/nuance/item/NuanceGitHubEventItemType.php b/src/applications/nuance/item/NuanceGitHubEventItemType.php index b8bfb3ccab..1f6249f222 100644 --- a/src/applications/nuance/item/NuanceGitHubEventItemType.php +++ b/src/applications/nuance/item/NuanceGitHubEventItemType.php @@ -309,6 +309,8 @@ final class NuanceGitHubEventItemType 'a', array( 'href' => $event_uri, + 'target' => '_blank', + 'rel' => 'noreferrer', ), $event_uri); } diff --git a/src/applications/phriction/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php index de3064fd42..0c6a2c23bc 100644 --- a/src/applications/phriction/query/PhrictionDocumentQuery.php +++ b/src/applications/phriction/query/PhrictionDocumentQuery.php @@ -10,6 +10,9 @@ final class PhrictionDocumentQuery private $slugPrefix; private $statuses; + private $parentPaths; + private $ancestorPaths; + private $needContent; const ORDER_HIERARCHY = 'hierarchy'; @@ -34,7 +37,7 @@ final class PhrictionDocumentQuery return $this; } - public function withSlugPrefix($slug_prefix) { + public function withSlugPrefix($slug_prefix) { $this->slugPrefix = $slug_prefix; return $this; } @@ -44,6 +47,16 @@ final class PhrictionDocumentQuery return $this; } + public function withParentPaths(array $paths) { + $this->parentPaths = $paths; + return $this; + } + + public function withAncestorPaths(array $paths) { + $this->ancestorPaths = $paths; + return $this; + } + public function needContent($need_content) { $this->needContent = $need_content; return $this; @@ -214,6 +227,94 @@ final class PhrictionDocumentQuery $this->depths); } + if ($this->parentPaths !== null || $this->ancestorPaths !== null) { + $sets = array( + array( + 'paths' => $this->parentPaths, + 'parents' => true, + ), + array( + 'paths' => $this->ancestorPaths, + 'parents' => false, + ), + ); + + $paths = array(); + foreach ($sets as $set) { + $set_paths = $set['paths']; + if ($set_paths === null) { + continue; + } + + if (!$set_paths) { + throw new PhabricatorEmptyQueryException( + pht('No parent/ancestor paths specified.')); + } + + $is_parents = $set['parents']; + foreach ($set_paths as $path) { + $path_normal = PhabricatorSlug::normalize($path); + if ($path !== $path_normal) { + throw new Exception( + pht( + 'Document path "%s" is not a valid path. The normalized '. + 'form of this path is "%s".', + $path, + $path_normal)); + } + + $depth = PhabricatorSlug::getDepth($path_normal); + if ($is_parents) { + $min_depth = $depth + 1; + $max_depth = $depth + 1; + } else { + $min_depth = $depth + 1; + $max_depth = null; + } + + $paths[] = array( + $path_normal, + $min_depth, + $max_depth, + ); + } + } + + $path_clauses = array(); + foreach ($paths as $path) { + $parts = array(); + list($prefix, $min, $max) = $path; + + // If we're getting children or ancestors of the root document, they + // aren't actually stored with the leading "/" in the database, so + // just skip this part of the clause. + if ($prefix !== '/') { + $parts[] = qsprintf( + $conn, + 'd.slug LIKE %>', + $prefix); + } + + if ($min !== null) { + $parts[] = qsprintf( + $conn, + 'd.depth >= %d', + $min); + } + + if ($max !== null) { + $parts[] = qsprintf( + $conn, + 'd.depth <= %d', + $max); + } + + $path_clauses[] = '('.implode(') AND (', $parts).')'; + } + + $where[] = '('.implode(') OR (', $path_clauses).')'; + } + return $where; } diff --git a/src/applications/phriction/query/PhrictionDocumentSearchEngine.php b/src/applications/phriction/query/PhrictionDocumentSearchEngine.php index e0781ec81f..e3d962146a 100644 --- a/src/applications/phriction/query/PhrictionDocumentSearchEngine.php +++ b/src/applications/phriction/query/PhrictionDocumentSearchEngine.php @@ -27,6 +27,14 @@ final class PhrictionDocumentSearchEngine $query->withSlugs($map['paths']); } + if ($map['parentPaths']) { + $query->withParentPaths($map['parentPaths']); + } + + if ($map['ancestorPaths']) { + $query->withAncestorPaths($map['ancestorPaths']); + } + return $query; } @@ -40,6 +48,14 @@ final class PhrictionDocumentSearchEngine ->setKey('paths') ->setIsHidden(true) ->setLabel(pht('Paths')), + id(new PhabricatorSearchStringListField()) + ->setKey('parentPaths') + ->setIsHidden(true) + ->setLabel(pht('Parent Paths')), + id(new PhabricatorSearchStringListField()) + ->setKey('ancestorPaths') + ->setIsHidden(true) + ->setLabel(pht('Ancestor Paths')), ); } diff --git a/src/applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php b/src/applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php index 4ef59b300c..c4ffc366da 100644 --- a/src/applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php +++ b/src/applications/phurl/remarkup/PhabricatorPhurlLinkRemarkupRule.php @@ -64,6 +64,7 @@ final class PhabricatorPhurlLinkRemarkupRule extends PhutilRemarkupRule { array( 'href' => $uri, 'target' => '_blank', + 'rel' => 'noreferrer', ), $name); } diff --git a/src/applications/ponder/application/PhabricatorPonderApplication.php b/src/applications/ponder/application/PhabricatorPonderApplication.php index ed37c7ef6e..56973447f9 100644 --- a/src/applications/ponder/application/PhabricatorPonderApplication.php +++ b/src/applications/ponder/application/PhabricatorPonderApplication.php @@ -18,12 +18,6 @@ final class PhabricatorPonderApplication extends PhabricatorApplication { return 'fa-university'; } - public function getFactObjectsForAnalysis() { - return array( - new PonderQuestion(), - ); - } - public function getTitleGlyph() { return "\xE2\x97\xB3"; } diff --git a/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php index f790816c8b..0b6a2f330e 100644 --- a/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php @@ -99,7 +99,8 @@ final class PhabricatorLinkProfileMenuItem ->setHref($href) ->setName($name) ->setIcon($icon_class) - ->setTooltip($tooltip); + ->setTooltip($tooltip) + ->setRel('noreferrer'); return array( $item, diff --git a/src/applications/settings/setting/PhabricatorFiletreeWidthSetting.php b/src/applications/settings/setting/PhabricatorFiletreeWidthSetting.php new file mode 100644 index 0000000000..5cfc9f3fe7 --- /dev/null +++ b/src/applications/settings/setting/PhabricatorFiletreeWidthSetting.php @@ -0,0 +1,12 @@ +oldValueHasBeenSet; } + public function newChronologicalSortVector() { + return id(new PhutilSortVector()) + ->addInt((int)$this->getDateCreated()) + ->addInt((int)$this->getID()); + } /* -( Rendering )---------------------------------------------------------- */ diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php index 3084b2d434..27b30a6278 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php @@ -19,9 +19,25 @@ final class PhabricatorTypeaheadFunctionHelpController return new Aphront404Response(); } - $source = $sources[$class]; + $raw_parameters = $request->getStr('parameters'); + if ($raw_parameters) { + $parameters = phutil_json_decode($raw_parameters); + } else { + $parameters = array(); + } + + $source = id(clone $sources[$class]) + ->setParameters($parameters); + + // This can fail for some types of datasources (like the custom field proxy + // datasources) if the "parameters" are wrong. Just fail cleanly instead + // of fataling. + try { + $application_class = $source->getDatasourceApplicationClass(); + } catch (Exception $ex) { + return new Aphront404Response(); + } - $application_class = $source->getDatasourceApplicationClass(); if ($application_class) { $result = id(new PhabricatorApplicationQuery()) ->setViewer($this->getViewer()) diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php index 2f55c26384..e0ba9a763b 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php @@ -226,6 +226,12 @@ final class PhabricatorTypeaheadModularDatasourceController if ($source->getAllDatasourceFunctions()) { $reference_uri = '/typeahead/help/'.get_class($source).'/'; + $parameters = $source->getParameters(); + if ($parameters) { + $reference_uri = (string)id(new PhutilURI($reference_uri)) + ->setQueryParam('parameters', phutil_json_encode($parameters)); + } + $reference_link = phutil_tag( 'a', array( diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php new file mode 100644 index 0000000000..3feb2d054d --- /dev/null +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadProxyDatasource.php @@ -0,0 +1,58 @@ +datasource = $datasource; + $this->setParameters( + array( + 'class' => get_class($datasource), + 'parameters' => $datasource->getParameters(), + )); + return $this; + } + + public function getDatasource() { + if (!$this->datasource) { + $class = $this->getParameter('class'); + + $parent = 'PhabricatorTypeaheadDatasource'; + if (!is_subclass_of($class, $parent)) { + throw new Exception( + pht( + 'Configured datasource class "%s" must be a valid subclass of '. + '"%s".', + $class, + $parent)); + } + + $datasource = newv($class, array()); + $datasource->setParameters($this->getParameter('parameters', array())); + $this->datasource = $datasource; + } + + return $this->datasource; + } + + public function getComponentDatasources() { + return array( + $this->getDatasource(), + ); + } + + public function getDatasourceApplicationClass() { + return $this->getDatasource()->getDatasourceApplicationClass(); + } + + public function getBrowseTitle() { + return $this->getDatasource()->getBrowseTitle(); + } + + public function getPlaceholderText() { + return $this->getDatasource()->getPlaceholderText(); + } + +} diff --git a/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php new file mode 100644 index 0000000000..31f189988c --- /dev/null +++ b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php @@ -0,0 +1,70 @@ + array( + 'name' => pht('Any Value'), + 'summary' => pht('Find results with any value.'), + 'description' => pht( + "This function includes results which have any value. Use a query ". + "like this to find results with any value:\n\n%s", + '> any()'), + ), + ); + } + + public function loadResults() { + $results = array( + $this->newAnyFunction(), + ); + return $this->filterResultsAgainstTokens($results); + } + + protected function evaluateFunction($function, array $argv_list) { + $results = array(); + + foreach ($argv_list as $argv) { + $results[] = new PhabricatorQueryConstraint( + PhabricatorQueryConstraint::OPERATOR_ANY, + null); + } + + return $results; + } + + public function renderFunctionTokens($function, array $argv_list) { + $results = array(); + foreach ($argv_list as $argv) { + $results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( + $this->newAnyFunction()); + } + return $results; + } + + private function newAnyFunction() { + $name = pht('Any Value'); + return $this->newFunctionResult() + ->setName($name.' any') + ->setDisplayName($name) + ->setIcon('fa-circle-o') + ->setPHID('any()') + ->setUnique(true) + ->addAttribute(pht('Select results with any value.')); + } + +} diff --git a/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php new file mode 100644 index 0000000000..7ba10e84ff --- /dev/null +++ b/src/infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchDatasource.php @@ -0,0 +1,17 @@ + array( + 'name' => pht('No Value'), + 'summary' => pht('Find results with no value.'), + 'description' => pht( + "This function includes results which have no value. Use a query ". + "like this to find results with no value:\n\n%s\n\n", + 'If you combine this function with other constraints, results '. + 'which have no value or the specified values will be returned.', + '> any()'), + ), + ); + } + + public function loadResults() { + $results = array( + $this->newNoneFunction(), + ); + return $this->filterResultsAgainstTokens($results); + } + + protected function evaluateFunction($function, array $argv_list) { + $results = array(); + + foreach ($argv_list as $argv) { + $results[] = new PhabricatorQueryConstraint( + PhabricatorQueryConstraint::OPERATOR_NULL, + null); + } + + return $results; + } + + public function renderFunctionTokens($function, array $argv_list) { + $results = array(); + foreach ($argv_list as $argv) { + $results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( + $this->newNoneFunction()); + } + return $results; + } + + private function newNoneFunction() { + $name = pht('No Value'); + return $this->newFunctionResult() + ->setName($name.' none') + ->setDisplayName($name) + ->setIcon('fa-ban') + ->setPHID('none()') + ->setUnique(true) + ->addAttribute(pht('Select results with no value.')); + } + +} diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php index c2b9d6543c..146f34ab07 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php @@ -31,7 +31,11 @@ final class PhabricatorStandardCustomFieldLink return phutil_tag( 'a', - array('href' => $value, 'target' => '_blank'), + array( + 'href' => $value, + 'target' => '_blank', + 'rel' => 'noreferrer', + ), $value); } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php index cdb54fb7e9..9bf59d41f6 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php @@ -33,12 +33,28 @@ abstract class PhabricatorStandardCustomFieldTokenizer $control = id(new AphrontFormTokenizerControl()) ->setLabel($this->getFieldName()) ->setName($this->getFieldKey()) - ->setDatasource($this->getDatasource()) + ->setDatasource($this->newApplicationSearchDatasource()) ->setValue(nonempty($value, array())); $form->appendControl($control); } + public function applyApplicationSearchConstraintToQuery( + PhabricatorApplicationSearchEngine $engine, + PhabricatorCursorPagedPolicyAwareQuery $query, + $value) { + if ($value) { + + $datasource = $this->newApplicationSearchDatasource() + ->setViewer($this->getViewer()); + $value = $datasource->evaluateTokens($value); + + $query->withApplicationSearchContainsConstraint( + $this->newStringIndex(null), + $value); + } + } + public function getHeraldFieldValueType($condition) { return id(new HeraldTokenizerFieldValue()) ->setKey('custom.'.$this->getFieldKey()) @@ -120,4 +136,11 @@ abstract class PhabricatorStandardCustomFieldTokenizer } } + protected function newApplicationSearchDatasource() { + $datasource = $this->getDatasource(); + + return id(new PhabricatorCustomFieldApplicationSearchDatasource()) + ->setDatasource($datasource); + } + } diff --git a/src/infrastructure/query/constraint/PhabricatorQueryConstraint.php b/src/infrastructure/query/constraint/PhabricatorQueryConstraint.php index 54cd7ae51f..874d8756e8 100644 --- a/src/infrastructure/query/constraint/PhabricatorQueryConstraint.php +++ b/src/infrastructure/query/constraint/PhabricatorQueryConstraint.php @@ -9,6 +9,7 @@ final class PhabricatorQueryConstraint extends Phobject { const OPERATOR_ANCESTOR = 'ancestor'; const OPERATOR_EMPTY = 'empty'; const OPERATOR_ONLY = 'only'; + const OPERATOR_ANY = 'any'; private $operator; private $value; diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 63daa6df79..5193b14975 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -342,6 +342,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery $where[] = $this->buildSpacesWhereClause($conn); $where[] = $this->buildNgramsWhereClause($conn); $where[] = $this->buildFerretWhereClause($conn); + $where[] = $this->buildApplicationSearchWhereClause($conn); return $where; } @@ -1158,12 +1159,29 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery PhabricatorCustomFieldIndexStorage $index, $value) { + $values = (array)$value; + + $data_values = array(); + $constraint_values = array(); + foreach ($values as $value) { + if ($value instanceof PhabricatorQueryConstraint) { + $constraint_values[] = $value; + } else { + $data_values[] = $value; + } + } + + $alias = 'appsearch_'.count($this->applicationSearchConstraints); + $this->applicationSearchConstraints[] = array( 'type' => $index->getIndexValueType(), 'cond' => '=', 'table' => $index->getTableName(), 'index' => $index->getIndexKey(), - 'value' => $value, + 'alias' => $alias, + 'value' => $values, + 'data' => $data_values, + 'constraints' => $constraint_values, ); return $this; @@ -1203,11 +1221,14 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery 'int')); } + $alias = 'appsearch_'.count($this->applicationSearchConstraints); + $this->applicationSearchConstraints[] = array( 'type' => $index->getIndexValueType(), 'cond' => 'range', 'table' => $index->getTableName(), 'index' => $index->getIndexKey(), + 'alias' => $alias, 'value' => array($min, $max), ); @@ -1256,7 +1277,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery switch ($type) { case 'string': case 'int': - if (count((array)$value) > 1) { + if (count($value) > 1) { return true; } break; @@ -1309,49 +1330,39 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery * @task appsearch */ protected function buildApplicationSearchJoinClause( - AphrontDatabaseConnection $conn_r) { + AphrontDatabaseConnection $conn) { $joins = array(); foreach ($this->applicationSearchConstraints as $key => $constraint) { $table = $constraint['table']; - $alias = 'appsearch_'.$key; + $alias = $constraint['alias']; $index = $constraint['index']; $cond = $constraint['cond']; $phid_column = $this->getApplicationSearchObjectPHIDColumn(); switch ($cond) { case '=': - $type = $constraint['type']; - switch ($type) { - case 'string': - $constraint_clause = qsprintf( - $conn_r, - '%T.indexValue IN (%Ls)', - $alias, - (array)$constraint['value']); + // Figure out whether we need to do a LEFT JOIN or not. We need to + // LEFT JOIN if we're going to select "IS NULL" rows. + $join_type = 'JOIN'; + foreach ($constraint['constraints'] as $query_constraint) { + $op = $query_constraint->getOperator(); + if ($op === PhabricatorQueryConstraint::OPERATOR_NULL) { + $join_type = 'LEFT JOIN'; break; - case 'int': - $constraint_clause = qsprintf( - $conn_r, - '%T.indexValue IN (%Ld)', - $alias, - (array)$constraint['value']); - break; - default: - throw new Exception(pht('Unknown index type "%s"!', $type)); + } } $joins[] = qsprintf( - $conn_r, - 'JOIN %T %T ON %T.objectPHID = %Q - AND %T.indexKey = %s - AND (%Q)', + $conn, + '%Q %T %T ON %T.objectPHID = %Q + AND %T.indexKey = %s', + $join_type, $table, $alias, $alias, $phid_column, $alias, - $index, - $constraint_clause); + $index); break; case 'range': list($min, $max) = $constraint['value']; @@ -1362,19 +1373,19 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery if ($min === null) { $constraint_clause = qsprintf( - $conn_r, + $conn, '%T.indexValue <= %d', $alias, $max); } else if ($max === null) { $constraint_clause = qsprintf( - $conn_r, + $conn, '%T.indexValue >= %d', $alias, $min); } else { $constraint_clause = qsprintf( - $conn_r, + $conn, '%T.indexValue BETWEEN %d AND %d', $alias, $min, @@ -1382,7 +1393,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery } $joins[] = qsprintf( - $conn_r, + $conn, 'JOIN %T %T ON %T.objectPHID = %Q AND %T.indexKey = %s AND (%Q)', @@ -1414,7 +1425,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery $key = $spec['customfield.index.key']; $joins[] = qsprintf( - $conn_r, + $conn, 'LEFT JOIN %T %T ON %T.objectPHID = %Q AND %T.indexKey = %s', $table, @@ -1428,6 +1439,88 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery return implode(' ', $joins); } + /** + * Construct a WHERE clause appropriate for applying ApplicationSearch + * constraints. + * + * @param AphrontDatabaseConnection Connection executing the query. + * @return list Where clause parts. + * @task appsearch + */ + protected function buildApplicationSearchWhereClause( + AphrontDatabaseConnection $conn) { + + $where = array(); + + foreach ($this->applicationSearchConstraints as $key => $constraint) { + $alias = $constraint['alias']; + $cond = $constraint['cond']; + $type = $constraint['type']; + + $data_values = $constraint['data']; + $constraint_values = $constraint['constraints']; + + $constraint_parts = array(); + switch ($cond) { + case '=': + if ($data_values) { + switch ($type) { + case 'string': + $constraint_parts[] = qsprintf( + $conn, + '%T.indexValue IN (%Ls)', + $alias, + $data_values); + break; + case 'int': + $constraint_parts[] = qsprintf( + $conn, + '%T.indexValue IN (%Ld)', + $alias, + $data_values); + break; + default: + throw new Exception(pht('Unknown index type "%s"!', $type)); + } + } + + if ($constraint_values) { + foreach ($constraint_values as $value) { + $op = $value->getOperator(); + switch ($op) { + case PhabricatorQueryConstraint::OPERATOR_NULL: + $constraint_parts[] = qsprintf( + $conn, + '%T.indexValue IS NULL', + $alias); + break; + case PhabricatorQueryConstraint::OPERATOR_ANY: + $constraint_parts[] = qsprintf( + $conn, + '%T.indexValue IS NOT NULL', + $alias); + break; + default: + throw new Exception( + pht( + 'No support for applying operator "%s" against '. + 'index of type "%s".', + $op, + $type)); + } + } + } + + if ($constraint_parts) { + $where[] = '('.implode(') OR (', $constraint_parts).')'; + } + break; + } + } + + return $where; + } + /* -( Integration with CustomField )--------------------------------------- */ diff --git a/src/view/layout/AphrontSideNavFilterView.php b/src/view/layout/AphrontSideNavFilterView.php index 6564fab196..9abcea5a02 100644 --- a/src/view/layout/AphrontSideNavFilterView.php +++ b/src/view/layout/AphrontSideNavFilterView.php @@ -30,6 +30,7 @@ final class AphrontSideNavFilterView extends AphrontView { private $mainID; private $isProfileMenu; private $footer = array(); + private $width; public function setMenuID($menu_id) { $this->menuID = $menu_id; @@ -82,6 +83,11 @@ final class AphrontSideNavFilterView extends AphrontView { return $this; } + public function setWidth($width) { + $this->width = $width; + return $this; + } + public function getMenuView() { return $this->menu; } @@ -216,6 +222,24 @@ final class AphrontSideNavFilterView extends AphrontView { $local_menu = null; $main_id = $this->getMainID(); + $width = $this->width; + if ($width) { + $width = min($width, 600); + $width = max($width, 150); + } else { + $width = null; + } + + if ($width && !$this->collapsed) { + $width_drag_style = 'left: '.$width.'px'; + $width_panel_style = 'width: '.$width.'px'; + $width_margin_style = 'margin-left: '.($width + 7).'px'; + } else { + $width_drag_style = null; + $width_panel_style = null; + $width_margin_style = null; + } + if ($this->flexible) { $drag_id = celerity_generate_unique_node_id(); $flex_bar = phutil_tag( @@ -223,6 +247,7 @@ final class AphrontSideNavFilterView extends AphrontView { array( 'class' => 'phabricator-nav-drag', 'id' => $drag_id, + 'style' => $width_drag_style, ), ''); } else { @@ -238,14 +263,14 @@ final class AphrontSideNavFilterView extends AphrontView { $nav_classes[] = 'has-local-nav'; } - $local_menu = - phutil_tag( - 'div', - array( - 'class' => 'phabricator-nav-local phabricator-side-menu', - 'id' => $local_id, - ), - $this->menu->setID($this->getMenuID())); + $local_menu = phutil_tag( + 'div', + array( + 'class' => 'phabricator-nav-local phabricator-side-menu', + 'id' => $local_id, + 'style' => $width_panel_style, + ), + $this->menu->setID($this->getMenuID())); } $crumbs = null; @@ -264,12 +289,13 @@ final class AphrontSideNavFilterView extends AphrontView { Javelin::initBehavior( 'phabricator-nav', array( - 'mainID' => $main_id, - 'localID' => $local_id, - 'dragID' => $drag_id, - 'contentID' => $content_id, - 'backgroundID' => $background_id, - 'collapsed' => $this->collapsed, + 'mainID' => $main_id, + 'localID' => $local_id, + 'dragID' => $drag_id, + 'contentID' => $content_id, + 'backgroundID' => $background_id, + 'collapsed' => $this->collapsed, + 'width' => $width, )); if ($this->active) { @@ -297,6 +323,7 @@ final class AphrontSideNavFilterView extends AphrontView { array( 'class' => 'phabricator-nav-content plb', 'id' => $content_id, + 'style' => $width_margin_style, ), array( $crumbs, diff --git a/src/view/layout/PhabricatorActionView.php b/src/view/layout/PhabricatorActionView.php index f6de8eca5b..a1d8fe2664 100644 --- a/src/view/layout/PhabricatorActionView.php +++ b/src/view/layout/PhabricatorActionView.php @@ -255,8 +255,10 @@ final class PhabricatorActionView extends AphrontView { } else { if ($this->getOpenInNewWindow()) { $target = '_blank'; + $rel = 'noreferrer'; } else { $target = null; + $rel = null; } if ($this->submenu) { @@ -277,6 +279,7 @@ final class PhabricatorActionView extends AphrontView { 'href' => $this->getHref(), 'class' => 'phabricator-action-view-item', 'target' => $target, + 'rel' => $rel, 'sigil' => $sigils, 'meta' => $this->metadata, ), diff --git a/src/view/phui/PHUIListItemView.php b/src/view/phui/PHUIListItemView.php index e8a1940737..8e53024826 100644 --- a/src/view/phui/PHUIListItemView.php +++ b/src/view/phui/PHUIListItemView.php @@ -34,6 +34,7 @@ final class PHUIListItemView extends AphrontTagView { private $actionIcon; private $actionIconHref; private $count; + private $rel; public function setOpenInNewWindow($open_in_new_window) { $this->openInNewWindow = $open_in_new_window; @@ -44,7 +45,16 @@ final class PHUIListItemView extends AphrontTagView { return $this->openInNewWindow; } - public function setHideInApplicationMenu($hide) { + public function setRel($rel) { + $this->rel = $rel; + return $this; + } + + public function getRel() { + return $this->rel; + } + + public function setHideInApplicationMenu($hide) { $this->hideInApplicationMenu = $hide; return $this; } @@ -363,6 +373,7 @@ final class PHUIListItemView extends AphrontTagView { 'meta' => $meta, 'sigil' => $sigil, 'target' => $this->getOpenInNewWindow() ? '_blank' : null, + 'rel' => $this->rel, ), array( $aural, diff --git a/src/view/phui/PHUITagView.php b/src/view/phui/PHUITagView.php index 292246c4a7..482b3f4400 100644 --- a/src/view/phui/PHUITagView.php +++ b/src/view/phui/PHUITagView.php @@ -154,25 +154,30 @@ final class PHUITagView extends AphrontTagView { $classes[] = 'phui-tag-'.$this->border; } - if ($this->phid) { - Javelin::initBehavior('phui-hovercards'); + $attributes = array( + 'href' => $this->href, + 'class' => $classes, + ); - $attributes = array( - 'href' => $this->href, - 'sigil' => 'hovercard', - 'meta' => array( - 'hoverPHID' => $this->phid, - ), - 'target' => $this->external ? '_blank' : null, - ); - } else { - $attributes = array( - 'href' => $this->href, - 'target' => $this->external ? '_blank' : null, + if ($this->external) { + $attributes += array( + 'target' => '_blank', + 'rel' => 'noreferrer', ); } - return $attributes + array('class' => $classes); + if ($this->phid) { + Javelin::initBehavior('phui-hovercards'); + + $attributes += array( + 'sigil' => 'hovercard', + 'meta' => array( + 'hoverPHID' => $this->phid, + ), + ); + } + + return $attributes; } protected function getTagContent() { diff --git a/webroot/rsrc/css/aphront/phabricator-nav-view.css b/webroot/rsrc/css/aphront/phabricator-nav-view.css index f3320e3eae..6dbd0931a6 100644 --- a/webroot/rsrc/css/aphront/phabricator-nav-view.css +++ b/webroot/rsrc/css/aphront/phabricator-nav-view.css @@ -44,7 +44,7 @@ position: fixed; top: 0; bottom: 0; - left: 410px; + left: 310px; width: 7px; cursor: col-resize; @@ -66,7 +66,7 @@ .device-desktop .phabricator-standard-page-body .has-drag-nav .phabricator-nav-content { - margin-left: 417px; + margin-left: 317px; } .device-desktop .phabricator-standard-page-body .has-drag-nav @@ -81,7 +81,7 @@ } .device-desktop .phui-navigation-shell .has-drag-nav .phabricator-nav-local { - width: 410px; + width: 310px; padding: 0; background: transparent; } diff --git a/webroot/rsrc/js/core/behavior-phabricator-nav.js b/webroot/rsrc/js/core/behavior-phabricator-nav.js index 74909e447d..50d8e1c820 100644 --- a/webroot/rsrc/js/core/behavior-phabricator-nav.js +++ b/webroot/rsrc/js/core/behavior-phabricator-nav.js @@ -18,7 +18,6 @@ JX.behavior('phabricator-nav', function(config) { var main = JX.$(config.mainID); var drag = JX.$(config.dragID); - // - Flexible Navigation Column ------------------------------------------------ @@ -98,22 +97,52 @@ JX.behavior('phabricator-nav', function(config) { } JX.DOM.alterClass(document.body, 'jx-drag-col', false); dragging = false; + + new JX.Request('/settings/adjust/', JX.bag) + .setData( + { + key: 'filetree.width', + value: JX.$V(drag).x + }) + .send(); }); - function resetdrag() { + var saved_width = config.width; + function savedrag() { + saved_width = JX.$V(drag).x; + local.style.width = ''; drag.style.left = ''; content.style.marginLeft = ''; } + function restoredrag() { + if (!saved_width) { + return; + } + + local.style.width = saved_width + 'px'; + drag.style.left = saved_width + 'px'; + content.style.marginLeft = (saved_width + JX.Vector.getDim(drag).x) + 'px'; + } + var collapsed = config.collapsed; JX.Stratcom.listen('differential-filetree-toggle', null, function() { collapsed = !collapsed; + + if (collapsed) { + savedrag(); + } + JX.DOM.alterClass(main, 'has-local-nav', !collapsed); JX.DOM.alterClass(main, 'has-drag-nav', !collapsed); JX.DOM.alterClass(main, 'has-closed-nav', collapsed); - resetdrag(); + + if (!collapsed) { + restoredrag(); + } + new JX.Request('/settings/adjust/', JX.bag) .setData({ key : 'nav-collapsed', value : (collapsed ? 1 : 0) }) .send();