diff --git a/resources/celerity/map.php b/resources/celerity/map.php index e0bb41b8aa..6c1240fea0 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,10 +9,10 @@ return array( 'names' => array( 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', - 'core.pkg.css' => '49b87886', - 'core.pkg.js' => '1ea38af8', - 'differential.pkg.css' => '113e692c', - 'differential.pkg.js' => 'f6d809c0', + 'core.pkg.css' => '39061f68', + 'core.pkg.js' => 'e1f0f7bd', + 'differential.pkg.css' => '06dc617c', + 'differential.pkg.js' => 'c2ca903a', 'diffusion.pkg.css' => 'a2d17c7d', 'diffusion.pkg.js' => '6134c5a1', 'maniphest.pkg.css' => '4845691a', @@ -29,7 +29,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' => 'a9e3e6d5', + 'rsrc/css/aphront/phabricator-nav-view.css' => '694d7723', 'rsrc/css/aphront/table-view.css' => '8c9bbafe', 'rsrc/css/aphront/tokenizer.css' => '15d5ff71', 'rsrc/css/aphront/tooltip.css' => '173b9431', @@ -61,7 +61,7 @@ return array( 'rsrc/css/application/dashboard/dashboard.css' => 'fe5b1869', 'rsrc/css/application/diff/inline-comment-summary.css' => 'f23d4e8f', 'rsrc/css/application/differential/add-comment.css' => 'c47f8c40', - 'rsrc/css/application/differential/changeset-view.css' => 'bf84345b', + 'rsrc/css/application/differential/changeset-view.css' => 'db34a142', 'rsrc/css/application/differential/core.css' => '5b7b8ff4', 'rsrc/css/application/differential/phui-inline-comment.css' => '65ae3bc2', 'rsrc/css/application/differential/revision-comment.css' => '14b8565a', @@ -71,7 +71,6 @@ return array( 'rsrc/css/application/diffusion/diffusion-icons.css' => '0c15255e', 'rsrc/css/application/diffusion/diffusion-readme.css' => '419dd5b6', 'rsrc/css/application/diffusion/diffusion-repository.css' => 'ee6f20ec', - 'rsrc/css/application/diffusion/diffusion-source.css' => '5f35a3bd', 'rsrc/css/application/diffusion/diffusion.css' => '45727264', 'rsrc/css/application/feed/feed.css' => 'ecd4ec57', 'rsrc/css/application/files/global-drag-and-drop.css' => 'b556a948', @@ -112,15 +111,15 @@ return array( 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 'rsrc/css/application/uiexample/example.css' => '528b19de', 'rsrc/css/core/core.css' => '62fa3ace', - 'rsrc/css/core/remarkup.css' => '1828e2ad', - 'rsrc/css/core/syntax.css' => 'cae95e89', + 'rsrc/css/core/remarkup.css' => 'bff43c81', + 'rsrc/css/core/syntax.css' => 'e9c95dd4', 'rsrc/css/core/z-index.css' => '9d8f7c4b', 'rsrc/css/diviner/diviner-shared.css' => '896f1d43', 'rsrc/css/font/font-awesome.css' => 'e838e088', 'rsrc/css/font/font-lato.css' => 'c7ccd872', 'rsrc/css/font/phui-font-icon-base.css' => '870a7360', 'rsrc/css/layout/phabricator-filetree-view.css' => 'b912ad97', - 'rsrc/css/layout/phabricator-source-code-view.css' => '31ee3c83', + 'rsrc/css/layout/phabricator-source-code-view.css' => '09368218', 'rsrc/css/phui/button/phui-button-bar.css' => 'f1ff5494', 'rsrc/css/phui/button/phui-button-simple.css' => '8e1baf68', 'rsrc/css/phui/button/phui-button.css' => '1863cc6e', @@ -386,13 +385,11 @@ return array( 'rsrc/js/application/diffusion/behavior-commit-branches.js' => 'bdaf4d04', 'rsrc/js/application/diffusion/behavior-commit-graph.js' => '75b83cbb', 'rsrc/js/application/diffusion/behavior-diffusion-browse-file.js' => '054a0f0b', - 'rsrc/js/application/diffusion/behavior-jump-to.js' => '73d09eef', - 'rsrc/js/application/diffusion/behavior-load-blame.js' => '42126667', 'rsrc/js/application/diffusion/behavior-locate-file.js' => '6d3e1947', 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'f01586dc', 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '1db13e70', 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef', - 'rsrc/js/application/files/behavior-document-engine.js' => '9108ee1a', + 'rsrc/js/application/files/behavior-document-engine.js' => '0333c0b6', 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '191b4909', @@ -423,7 +420,7 @@ return array( 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', 'rsrc/js/application/releeph/releeph-request-state-change.js' => 'a0b57eb8', 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'de2e896f', - 'rsrc/js/application/repository/repository-crossreference.js' => '2ab10a76', + 'rsrc/js/application/repository/repository-crossreference.js' => '9a860428', 'rsrc/js/application/search/behavior-reorder-profile-menu-items.js' => 'e2e0a072', 'rsrc/js/application/search/behavior-reorder-queries.js' => 'e9581f08', 'rsrc/js/application/slowvote/behavior-slowvote-embed.js' => '887ad43f', @@ -473,11 +470,11 @@ return array( 'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0', 'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0', 'rsrc/js/core/behavior-lightbox-attachments.js' => '6b31879a', - 'rsrc/js/core/behavior-line-linker.js' => '13e39479', + 'rsrc/js/core/behavior-line-linker.js' => '66a62306', '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' => '836f966d', + 'rsrc/js/core/behavior-phabricator-nav.js' => '94b7c320', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'acd29eee', 'rsrc/js/core/behavior-read-only-warning.js' => 'ba158207', 'rsrc/js/core/behavior-redirect.js' => '0213259f', @@ -543,7 +540,7 @@ return array( 'conpherence-thread-manager' => '4d863052', 'conpherence-transaction-css' => '85129c68', 'd3' => 'a11a5ff2', - 'differential-changeset-view-css' => 'bf84345b', + 'differential-changeset-view-css' => 'db34a142', 'differential-core-view-css' => '5b7b8ff4', 'differential-revision-add-comment-css' => 'c47f8c40', 'differential-revision-comment-css' => '14b8565a', @@ -554,7 +551,6 @@ return array( 'diffusion-icons-css' => '0c15255e', 'diffusion-readme-css' => '419dd5b6', 'diffusion-repository-css' => 'ee6f20ec', - 'diffusion-source-css' => '5f35a3bd', 'diviner-shared-css' => '896f1d43', 'font-fontawesome' => 'e838e088', 'font-lato' => 'c7ccd872', @@ -604,10 +600,9 @@ return array( 'javelin-behavior-diffusion-browse-file' => '054a0f0b', 'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04', 'javelin-behavior-diffusion-commit-graph' => '75b83cbb', - 'javelin-behavior-diffusion-jump-to' => '73d09eef', 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc', - 'javelin-behavior-document-engine' => '9108ee1a', + 'javelin-behavior-document-engine' => '0333c0b6', 'javelin-behavior-doorkeeper-tag' => '1db13e70', 'javelin-behavior-drydock-live-operation-status' => '901935ef', 'javelin-behavior-durable-column' => '2ae077e1', @@ -624,7 +619,6 @@ return array( 'javelin-behavior-launch-icon-composer' => '48086888', 'javelin-behavior-lightbox-attachments' => '6b31879a', 'javelin-behavior-line-chart' => 'e4232876', - 'javelin-behavior-load-blame' => '42126667', 'javelin-behavior-maniphest-batch-selector' => 'ad54037e', 'javelin-behavior-maniphest-list-editor' => 'a9f88de2', 'javelin-behavior-maniphest-subpriority-editor' => '71237763', @@ -638,8 +632,8 @@ return array( 'javelin-behavior-phabricator-gesture-example' => '558829c2', 'javelin-behavior-phabricator-keyboard-pager' => 'a8da01f0', 'javelin-behavior-phabricator-keyboard-shortcuts' => '01fca1f0', - 'javelin-behavior-phabricator-line-linker' => '13e39479', - 'javelin-behavior-phabricator-nav' => '836f966d', + 'javelin-behavior-phabricator-line-linker' => '66a62306', + 'javelin-behavior-phabricator-nav' => '94b7c320', 'javelin-behavior-phabricator-notification-example' => '8ce821c5', 'javelin-behavior-phabricator-object-selector' => '77c1f0b0', 'javelin-behavior-phabricator-oncopy' => '2926fff2', @@ -676,7 +670,7 @@ return array( 'javelin-behavior-reorder-applications' => '76b9fc3e', 'javelin-behavior-reorder-columns' => 'e1d25dfb', 'javelin-behavior-reorder-profile-menu-items' => 'e2e0a072', - 'javelin-behavior-repository-crossreference' => '2ab10a76', + 'javelin-behavior-repository-crossreference' => '9a860428', 'javelin-behavior-scrollbar' => '834a1173', 'javelin-behavior-search-reorder-queries' => 'e9581f08', 'javelin-behavior-select-content' => 'bf5374ef', @@ -773,18 +767,18 @@ return array( 'phabricator-keyboard-shortcut' => '1ae869f2', 'phabricator-keyboard-shortcut-manager' => 'c19dd9b9', 'phabricator-main-menu-view' => '1802a242', - 'phabricator-nav-view-css' => 'a9e3e6d5', + 'phabricator-nav-view-css' => '694d7723', 'phabricator-notification' => '4f774dac', 'phabricator-notification-css' => '457861ec', 'phabricator-notification-menu-css' => '10685bd4', 'phabricator-object-selector-css' => '85ee8ce6', 'phabricator-phtize' => 'd254d646', 'phabricator-prefab' => '77b0ae28', - 'phabricator-remarkup-css' => '1828e2ad', + 'phabricator-remarkup-css' => 'bff43c81', 'phabricator-search-results-css' => '505dd8cf', 'phabricator-shaped-request' => '7cbe244b', 'phabricator-slowvote-css' => 'a94b7230', - 'phabricator-source-code-view-css' => '31ee3c83', + 'phabricator-source-code-view-css' => '09368218', 'phabricator-standard-page-view' => '34ee718b', 'phabricator-textareautils' => '320810c8', 'phabricator-title' => '485aaa6c', @@ -884,7 +878,7 @@ return array( 'sprite-login-css' => '396f3c3a', 'sprite-tokens-css' => '9cdfd599', 'syntax-default-css' => '9923583c', - 'syntax-highlighting-css' => 'cae95e89', + 'syntax-highlighting-css' => 'e9c95dd4', 'tokens-css' => '3d0f239e', 'typeahead-browse-css' => 'f2818435', 'unhandled-exception-css' => '4c96257a', @@ -913,6 +907,11 @@ return array( 'javelin-behavior', 'javelin-uri', ), + '0333c0b6' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + ), '040fce04' => array( 'javelin-behavior', 'javelin-request', @@ -964,12 +963,6 @@ return array( 'javelin-install', 'javelin-util', ), - '13e39479' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-dom', - 'javelin-history', - ), '15d5ff71' => array( 'aphront-typeahead-control-css', 'phui-tag-view-css', @@ -1056,12 +1049,6 @@ return array( 'javelin-install', 'javelin-util', ), - '2ab10a76' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-uri', - ), '2ae077e1' => array( 'javelin-behavior', 'javelin-dom', @@ -1165,11 +1152,6 @@ return array( 'phabricator-diff-changeset-list', 'phabricator-diff-changeset', ), - 42126667 => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-request', - ), '4250a34e' => array( 'javelin-behavior', 'javelin-dom', @@ -1405,6 +1387,12 @@ return array( 'phabricator-darklog', 'phabricator-darkmessage', ), + '66a62306' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + 'javelin-history', + ), '66a6def1' => array( 'javelin-behavior', 'javelin-dom', @@ -1481,11 +1469,6 @@ return array( 'javelin-behavior', 'javelin-dom', ), - '73d09eef' => array( - 'javelin-behavior', - 'javelin-vector', - 'javelin-dom', - ), '758b4758' => array( 'javelin-install', 'javelin-workboard-card', @@ -1557,16 +1540,6 @@ return array( 'javelin-behavior', 'javelin-scrollbar', ), - '836f966d' => array( - 'javelin-behavior', - 'javelin-behavior-device', - 'javelin-stratcom', - 'javelin-dom', - 'javelin-magical-init', - 'javelin-vector', - 'javelin-request', - 'javelin-util', - ), '8499b6ab' => array( 'javelin-behavior', 'javelin-dom', @@ -1644,11 +1617,6 @@ return array( 'javelin-stratcom', 'javelin-vector', ), - '9108ee1a' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - ), '92b9ec77' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1673,6 +1641,16 @@ return array( 'javelin-resource', 'javelin-routable', ), + '94b7c320' => array( + 'javelin-behavior', + 'javelin-behavior-device', + 'javelin-stratcom', + 'javelin-dom', + 'javelin-magical-init', + 'javelin-vector', + 'javelin-request', + 'javelin-util', + ), '960f6a39' => array( 'javelin-behavior', 'javelin-dom', @@ -1689,6 +1667,12 @@ return array( 'phuix-icon-view', 'javelin-behavior-phabricator-gesture', ), + '9a860428' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-uri', + ), '9bbf3762' => array( 'javelin-behavior', 'javelin-dom', @@ -1908,9 +1892,6 @@ return array( 'javelin-stratcom', 'javelin-dom', ), - 'bf84345b' => array( - 'phui-inline-comment-view-css', - ), 'bff6884b' => array( 'javelin-install', 'javelin-dom', @@ -1978,9 +1959,6 @@ return array( 'phabricator-title', 'phabricator-favicon', ), - 'cae95e89' => array( - 'syntax-default-css', - ), 'cd2b9b77' => array( 'phui-oi-list-view-css', ), @@ -2030,6 +2008,9 @@ return array( 'javelin-util', 'phabricator-shaped-request', ), + 'db34a142' => array( + 'phui-inline-comment-view-css', + ), 'dca75c0e' => array( 'multirow-row-manager', 'javelin-install', @@ -2109,6 +2090,9 @@ return array( 'javelin-dom', 'phabricator-draggable-list', ), + 'e9c95dd4' => array( + 'syntax-default-css', + ), 'ec1f3669' => array( 'javelin-behavior', 'javelin-util', @@ -2367,7 +2351,6 @@ return array( 'javelin-behavior-aphront-drag-and-drop-textarea', 'javelin-behavior-phabricator-object-selector', 'javelin-behavior-repository-crossreference', - 'javelin-behavior-load-blame', 'javelin-behavior-differential-user-select', 'javelin-behavior-aphront-more', 'phabricator-diff-inline', diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php index 3cec508678..958b1d6afa 100644 --- a/resources/celerity/packages.php +++ b/resources/celerity/packages.php @@ -199,7 +199,6 @@ return array( 'javelin-behavior-aphront-drag-and-drop-textarea', 'javelin-behavior-phabricator-object-selector', 'javelin-behavior-repository-crossreference', - 'javelin-behavior-load-blame', 'javelin-behavior-differential-user-select', 'javelin-behavior-aphront-more', diff --git a/resources/sql/autopatches/20180410.almanac.01.iface.xaction.sql b/resources/sql/autopatches/20180410.almanac.01.iface.xaction.sql new file mode 100644 index 0000000000..5f0dec18aa --- /dev/null +++ b/resources/sql/autopatches/20180410.almanac.01.iface.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_interfacetransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 391b8fb1f2..da5674cbbd 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -9,18 +9,27 @@ phutil_register_library_map(array( '__library_version__' => 2, 'class' => array( - 'AlamancServiceEditConduitAPIMethod' => 'applications/almanac/conduit/AlamancServiceEditConduitAPIMethod.php', 'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php', 'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php', + 'AlmanacBindingDeletePropertyTransaction' => 'applications/almanac/xaction/AlmanacBindingDeletePropertyTransaction.php', 'AlmanacBindingDisableController' => 'applications/almanac/controller/AlmanacBindingDisableController.php', + 'AlmanacBindingDisableTransaction' => 'applications/almanac/xaction/AlmanacBindingDisableTransaction.php', + 'AlmanacBindingEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacBindingEditConduitAPIMethod.php', 'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php', + 'AlmanacBindingEditEngine' => 'applications/almanac/editor/AlmanacBindingEditEngine.php', 'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php', + 'AlmanacBindingInterfaceTransaction' => 'applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php', 'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php', 'AlmanacBindingPropertyEditEngine' => 'applications/almanac/editor/AlmanacBindingPropertyEditEngine.php', 'AlmanacBindingQuery' => 'applications/almanac/query/AlmanacBindingQuery.php', + 'AlmanacBindingSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacBindingSearchConduitAPIMethod.php', + 'AlmanacBindingSearchEngine' => 'applications/almanac/query/AlmanacBindingSearchEngine.php', + 'AlmanacBindingServiceTransaction' => 'applications/almanac/xaction/AlmanacBindingServiceTransaction.php', + 'AlmanacBindingSetPropertyTransaction' => 'applications/almanac/xaction/AlmanacBindingSetPropertyTransaction.php', 'AlmanacBindingTableView' => 'applications/almanac/view/AlmanacBindingTableView.php', 'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php', 'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php', + 'AlmanacBindingTransactionType' => 'applications/almanac/xaction/AlmanacBindingTransactionType.php', 'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php', 'AlmanacBindingsSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php', 'AlmanacCacheEngineExtension' => 'applications/almanac/engineextension/AlmanacCacheEngineExtension.php', @@ -35,66 +44,94 @@ phutil_register_library_map(array( 'AlmanacCreateServicesCapability' => 'applications/almanac/capability/AlmanacCreateServicesCapability.php', 'AlmanacCustomServiceType' => 'applications/almanac/servicetype/AlmanacCustomServiceType.php', 'AlmanacDAO' => 'applications/almanac/storage/AlmanacDAO.php', + 'AlmanacDeletePropertyEditField' => 'applications/almanac/engineextension/AlmanacDeletePropertyEditField.php', + 'AlmanacDeletePropertyEditType' => 'applications/almanac/engineextension/AlmanacDeletePropertyEditType.php', 'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php', 'AlmanacDeviceController' => 'applications/almanac/controller/AlmanacDeviceController.php', + 'AlmanacDeviceDeletePropertyTransaction' => 'applications/almanac/xaction/AlmanacDeviceDeletePropertyTransaction.php', 'AlmanacDeviceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacDeviceEditConduitAPIMethod.php', 'AlmanacDeviceEditController' => 'applications/almanac/controller/AlmanacDeviceEditController.php', 'AlmanacDeviceEditEngine' => 'applications/almanac/editor/AlmanacDeviceEditEngine.php', 'AlmanacDeviceEditor' => 'applications/almanac/editor/AlmanacDeviceEditor.php', 'AlmanacDeviceListController' => 'applications/almanac/controller/AlmanacDeviceListController.php', 'AlmanacDeviceNameNgrams' => 'applications/almanac/storage/AlmanacDeviceNameNgrams.php', + 'AlmanacDeviceNameTransaction' => 'applications/almanac/xaction/AlmanacDeviceNameTransaction.php', 'AlmanacDevicePHIDType' => 'applications/almanac/phid/AlmanacDevicePHIDType.php', 'AlmanacDevicePropertyEditEngine' => 'applications/almanac/editor/AlmanacDevicePropertyEditEngine.php', 'AlmanacDeviceQuery' => 'applications/almanac/query/AlmanacDeviceQuery.php', 'AlmanacDeviceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacDeviceSearchConduitAPIMethod.php', 'AlmanacDeviceSearchEngine' => 'applications/almanac/query/AlmanacDeviceSearchEngine.php', + 'AlmanacDeviceSetPropertyTransaction' => 'applications/almanac/xaction/AlmanacDeviceSetPropertyTransaction.php', 'AlmanacDeviceTransaction' => 'applications/almanac/storage/AlmanacDeviceTransaction.php', 'AlmanacDeviceTransactionQuery' => 'applications/almanac/query/AlmanacDeviceTransactionQuery.php', + 'AlmanacDeviceTransactionType' => 'applications/almanac/xaction/AlmanacDeviceTransactionType.php', 'AlmanacDeviceViewController' => 'applications/almanac/controller/AlmanacDeviceViewController.php', 'AlmanacDrydockPoolServiceType' => 'applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php', 'AlmanacEditor' => 'applications/almanac/editor/AlmanacEditor.php', 'AlmanacInterface' => 'applications/almanac/storage/AlmanacInterface.php', + 'AlmanacInterfaceAddressTransaction' => 'applications/almanac/xaction/AlmanacInterfaceAddressTransaction.php', 'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php', 'AlmanacInterfaceDeleteController' => 'applications/almanac/controller/AlmanacInterfaceDeleteController.php', + 'AlmanacInterfaceDestroyTransaction' => 'applications/almanac/xaction/AlmanacInterfaceDestroyTransaction.php', + 'AlmanacInterfaceDeviceTransaction' => 'applications/almanac/xaction/AlmanacInterfaceDeviceTransaction.php', + 'AlmanacInterfaceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacInterfaceEditConduitAPIMethod.php', 'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php', + 'AlmanacInterfaceEditEngine' => 'applications/almanac/editor/AlmanacInterfaceEditEngine.php', + 'AlmanacInterfaceEditor' => 'applications/almanac/editor/AlmanacInterfaceEditor.php', + 'AlmanacInterfaceNetworkTransaction' => 'applications/almanac/xaction/AlmanacInterfaceNetworkTransaction.php', 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', + 'AlmanacInterfacePortTransaction' => 'applications/almanac/xaction/AlmanacInterfacePortTransaction.php', 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', + 'AlmanacInterfaceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacInterfaceSearchConduitAPIMethod.php', + 'AlmanacInterfaceSearchEngine' => 'applications/almanac/query/AlmanacInterfaceSearchEngine.php', 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', + 'AlmanacInterfaceTransaction' => 'applications/almanac/storage/AlmanacInterfaceTransaction.php', + 'AlmanacInterfaceTransactionType' => 'applications/almanac/xaction/AlmanacInterfaceTransactionType.php', 'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php', 'AlmanacManageClusterServicesCapability' => 'applications/almanac/capability/AlmanacManageClusterServicesCapability.php', 'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php', 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', + 'AlmanacModularTransaction' => 'applications/almanac/storage/AlmanacModularTransaction.php', 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', 'AlmanacNamesTestCase' => 'applications/almanac/util/__tests__/AlmanacNamesTestCase.php', 'AlmanacNamespace' => 'applications/almanac/storage/AlmanacNamespace.php', 'AlmanacNamespaceController' => 'applications/almanac/controller/AlmanacNamespaceController.php', + 'AlmanacNamespaceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNamespaceEditConduitAPIMethod.php', 'AlmanacNamespaceEditController' => 'applications/almanac/controller/AlmanacNamespaceEditController.php', 'AlmanacNamespaceEditEngine' => 'applications/almanac/editor/AlmanacNamespaceEditEngine.php', 'AlmanacNamespaceEditor' => 'applications/almanac/editor/AlmanacNamespaceEditor.php', 'AlmanacNamespaceListController' => 'applications/almanac/controller/AlmanacNamespaceListController.php', 'AlmanacNamespaceNameNgrams' => 'applications/almanac/storage/AlmanacNamespaceNameNgrams.php', + 'AlmanacNamespaceNameTransaction' => 'applications/almanac/xaction/AlmanacNamespaceNameTransaction.php', 'AlmanacNamespacePHIDType' => 'applications/almanac/phid/AlmanacNamespacePHIDType.php', 'AlmanacNamespaceQuery' => 'applications/almanac/query/AlmanacNamespaceQuery.php', + 'AlmanacNamespaceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNamespaceSearchConduitAPIMethod.php', 'AlmanacNamespaceSearchEngine' => 'applications/almanac/query/AlmanacNamespaceSearchEngine.php', 'AlmanacNamespaceTransaction' => 'applications/almanac/storage/AlmanacNamespaceTransaction.php', 'AlmanacNamespaceTransactionQuery' => 'applications/almanac/query/AlmanacNamespaceTransactionQuery.php', + 'AlmanacNamespaceTransactionType' => 'applications/almanac/xaction/AlmanacNamespaceTransactionType.php', 'AlmanacNamespaceViewController' => 'applications/almanac/controller/AlmanacNamespaceViewController.php', 'AlmanacNetwork' => 'applications/almanac/storage/AlmanacNetwork.php', 'AlmanacNetworkController' => 'applications/almanac/controller/AlmanacNetworkController.php', + 'AlmanacNetworkEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNetworkEditConduitAPIMethod.php', 'AlmanacNetworkEditController' => 'applications/almanac/controller/AlmanacNetworkEditController.php', 'AlmanacNetworkEditEngine' => 'applications/almanac/editor/AlmanacNetworkEditEngine.php', 'AlmanacNetworkEditor' => 'applications/almanac/editor/AlmanacNetworkEditor.php', 'AlmanacNetworkListController' => 'applications/almanac/controller/AlmanacNetworkListController.php', 'AlmanacNetworkNameNgrams' => 'applications/almanac/storage/AlmanacNetworkNameNgrams.php', + 'AlmanacNetworkNameTransaction' => 'applications/almanac/xaction/AlmanacNetworkNameTransaction.php', 'AlmanacNetworkPHIDType' => 'applications/almanac/phid/AlmanacNetworkPHIDType.php', 'AlmanacNetworkQuery' => 'applications/almanac/query/AlmanacNetworkQuery.php', + 'AlmanacNetworkSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacNetworkSearchConduitAPIMethod.php', 'AlmanacNetworkSearchEngine' => 'applications/almanac/query/AlmanacNetworkSearchEngine.php', 'AlmanacNetworkTransaction' => 'applications/almanac/storage/AlmanacNetworkTransaction.php', 'AlmanacNetworkTransactionQuery' => 'applications/almanac/query/AlmanacNetworkTransactionQuery.php', + 'AlmanacNetworkTransactionType' => 'applications/almanac/xaction/AlmanacNetworkTransactionType.php', 'AlmanacNetworkViewController' => 'applications/almanac/controller/AlmanacNetworkViewController.php', 'AlmanacPropertiesDestructionEngineExtension' => 'applications/almanac/engineextension/AlmanacPropertiesDestructionEngineExtension.php', + 'AlmanacPropertiesEditEngineExtension' => 'applications/almanac/engineextension/AlmanacPropertiesEditEngineExtension.php', 'AlmanacPropertiesSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacPropertiesSearchEngineAttachment.php', 'AlmanacProperty' => 'applications/almanac/storage/AlmanacProperty.php', 'AlmanacPropertyController' => 'applications/almanac/controller/AlmanacPropertyController.php', @@ -109,23 +146,31 @@ phutil_register_library_map(array( 'AlmanacService' => 'applications/almanac/storage/AlmanacService.php', 'AlmanacServiceController' => 'applications/almanac/controller/AlmanacServiceController.php', 'AlmanacServiceDatasource' => 'applications/almanac/typeahead/AlmanacServiceDatasource.php', + 'AlmanacServiceDeletePropertyTransaction' => 'applications/almanac/xaction/AlmanacServiceDeletePropertyTransaction.php', + 'AlmanacServiceEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacServiceEditConduitAPIMethod.php', 'AlmanacServiceEditController' => 'applications/almanac/controller/AlmanacServiceEditController.php', 'AlmanacServiceEditEngine' => 'applications/almanac/editor/AlmanacServiceEditEngine.php', 'AlmanacServiceEditor' => 'applications/almanac/editor/AlmanacServiceEditor.php', 'AlmanacServiceListController' => 'applications/almanac/controller/AlmanacServiceListController.php', 'AlmanacServiceNameNgrams' => 'applications/almanac/storage/AlmanacServiceNameNgrams.php', + 'AlmanacServiceNameTransaction' => 'applications/almanac/xaction/AlmanacServiceNameTransaction.php', 'AlmanacServicePHIDType' => 'applications/almanac/phid/AlmanacServicePHIDType.php', 'AlmanacServicePropertyEditEngine' => 'applications/almanac/editor/AlmanacServicePropertyEditEngine.php', 'AlmanacServiceQuery' => 'applications/almanac/query/AlmanacServiceQuery.php', 'AlmanacServiceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacServiceSearchConduitAPIMethod.php', 'AlmanacServiceSearchEngine' => 'applications/almanac/query/AlmanacServiceSearchEngine.php', + 'AlmanacServiceSetPropertyTransaction' => 'applications/almanac/xaction/AlmanacServiceSetPropertyTransaction.php', 'AlmanacServiceTransaction' => 'applications/almanac/storage/AlmanacServiceTransaction.php', 'AlmanacServiceTransactionQuery' => 'applications/almanac/query/AlmanacServiceTransactionQuery.php', + 'AlmanacServiceTransactionType' => 'applications/almanac/xaction/AlmanacServiceTransactionType.php', 'AlmanacServiceType' => 'applications/almanac/servicetype/AlmanacServiceType.php', 'AlmanacServiceTypeDatasource' => 'applications/almanac/typeahead/AlmanacServiceTypeDatasource.php', 'AlmanacServiceTypeTestCase' => 'applications/almanac/servicetype/__tests__/AlmanacServiceTypeTestCase.php', + 'AlmanacServiceTypeTransaction' => 'applications/almanac/xaction/AlmanacServiceTypeTransaction.php', 'AlmanacServiceViewController' => 'applications/almanac/controller/AlmanacServiceViewController.php', - 'AlmanacTransaction' => 'applications/almanac/storage/AlmanacTransaction.php', + 'AlmanacSetPropertyEditField' => 'applications/almanac/engineextension/AlmanacSetPropertyEditField.php', + 'AlmanacSetPropertyEditType' => 'applications/almanac/engineextension/AlmanacSetPropertyEditType.php', + 'AlmanacTransactionType' => 'applications/almanac/xaction/AlmanacTransactionType.php', 'AphlictDropdownDataQuery' => 'applications/aphlict/query/AphlictDropdownDataQuery.php', 'Aphront304Response' => 'aphront/response/Aphront304Response.php', 'Aphront400Response' => 'aphront/response/Aphront400Response.php', @@ -632,6 +677,7 @@ phutil_register_library_map(array( 'DiffusionAuditorsAddSelfHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php', 'DiffusionAuditorsHeraldAction' => 'applications/diffusion/herald/DiffusionAuditorsHeraldAction.php', 'DiffusionBlameConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionBlameConduitAPIMethod.php', + 'DiffusionBlameController' => 'applications/diffusion/controller/DiffusionBlameController.php', 'DiffusionBlameQuery' => 'applications/diffusion/query/blame/DiffusionBlameQuery.php', 'DiffusionBlockHeraldAction' => 'applications/diffusion/herald/DiffusionBlockHeraldAction.php', 'DiffusionBranchListView' => 'applications/diffusion/view/DiffusionBranchListView.php', @@ -732,6 +778,8 @@ phutil_register_library_map(array( 'DiffusionDiffController' => 'applications/diffusion/controller/DiffusionDiffController.php', 'DiffusionDiffInlineCommentQuery' => 'applications/diffusion/query/DiffusionDiffInlineCommentQuery.php', 'DiffusionDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php', + 'DiffusionDocumentController' => 'applications/diffusion/controller/DiffusionDocumentController.php', + 'DiffusionDocumentRenderingEngine' => 'applications/diffusion/document/DiffusionDocumentRenderingEngine.php', 'DiffusionDoorkeeperCommitFeedStoryPublisher' => 'applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php', 'DiffusionEmptyResultView' => 'applications/diffusion/view/DiffusionEmptyResultView.php', 'DiffusionExistsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionExistsQueryConduitAPIMethod.php', @@ -2066,6 +2114,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionWarningException' => 'applications/transactions/exception/PhabricatorApplicationTransactionWarningException.php', 'PhabricatorApplicationTransactionWarningResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionWarningResponse.php', 'PhabricatorApplicationUninstallController' => 'applications/meta/controller/PhabricatorApplicationUninstallController.php', + 'PhabricatorApplicationUninstallTransaction' => 'applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php', 'PhabricatorApplicationsApplication' => 'applications/meta/application/PhabricatorApplicationsApplication.php', 'PhabricatorApplicationsController' => 'applications/meta/controller/PhabricatorApplicationsController.php', 'PhabricatorApplicationsListController' => 'applications/meta/controller/PhabricatorApplicationsListController.php', @@ -2819,6 +2868,7 @@ phutil_register_library_map(array( 'PhabricatorDivinerApplication' => 'applications/diviner/application/PhabricatorDivinerApplication.php', 'PhabricatorDocumentEngine' => 'applications/files/document/PhabricatorDocumentEngine.php', 'PhabricatorDocumentRef' => 'applications/files/document/PhabricatorDocumentRef.php', + 'PhabricatorDocumentRenderingEngine' => 'applications/files/document/render/PhabricatorDocumentRenderingEngine.php', 'PhabricatorDoorkeeperApplication' => 'applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php', 'PhabricatorDraft' => 'applications/draft/storage/PhabricatorDraft.php', 'PhabricatorDraftDAO' => 'applications/draft/storage/PhabricatorDraftDAO.php', @@ -3010,6 +3060,7 @@ phutil_register_library_map(array( 'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php', 'PhabricatorFileDeleteTransaction' => 'applications/files/xaction/PhabricatorFileDeleteTransaction.php', 'PhabricatorFileDocumentController' => 'applications/files/controller/PhabricatorFileDocumentController.php', + 'PhabricatorFileDocumentRenderingEngine' => 'applications/files/document/render/PhabricatorFileDocumentRenderingEngine.php', 'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php', 'PhabricatorFileEditController' => 'applications/files/controller/PhabricatorFileEditController.php', 'PhabricatorFileEditEngine' => 'applications/files/editor/PhabricatorFileEditEngine.php', @@ -3185,6 +3236,7 @@ phutil_register_library_map(array( 'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php', 'PhabricatorInstructionsEditField' => 'applications/transactions/editfield/PhabricatorInstructionsEditField.php', 'PhabricatorIntConfigType' => 'applications/config/type/PhabricatorIntConfigType.php', + 'PhabricatorIntEditField' => 'applications/transactions/editfield/PhabricatorIntEditField.php', 'PhabricatorIntExportField' => 'infrastructure/export/field/PhabricatorIntExportField.php', 'PhabricatorInternalSetting' => 'applications/settings/setting/PhabricatorInternalSetting.php', 'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php', @@ -5156,7 +5208,6 @@ phutil_register_library_map(array( 'require_celerity_resource' => 'applications/celerity/api.php', ), 'xmap' => array( - 'AlamancServiceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'AlmanacAddress' => 'Phobject', 'AlmanacBinding' => array( 'AlmanacDAO', @@ -5165,16 +5216,27 @@ phutil_register_library_map(array( 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorExtendedPolicyInterface', + 'PhabricatorConduitResultInterface', ), + 'AlmanacBindingDeletePropertyTransaction' => 'AlmanacBindingTransactionType', 'AlmanacBindingDisableController' => 'AlmanacServiceController', + 'AlmanacBindingDisableTransaction' => 'AlmanacBindingTransactionType', + 'AlmanacBindingEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'AlmanacBindingEditController' => 'AlmanacServiceController', + 'AlmanacBindingEditEngine' => 'PhabricatorEditEngine', 'AlmanacBindingEditor' => 'AlmanacEditor', + 'AlmanacBindingInterfaceTransaction' => 'AlmanacBindingTransactionType', 'AlmanacBindingPHIDType' => 'PhabricatorPHIDType', 'AlmanacBindingPropertyEditEngine' => 'AlmanacPropertyEditEngine', 'AlmanacBindingQuery' => 'AlmanacQuery', + 'AlmanacBindingSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', + 'AlmanacBindingSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'AlmanacBindingServiceTransaction' => 'AlmanacBindingTransactionType', + 'AlmanacBindingSetPropertyTransaction' => 'AlmanacBindingTransactionType', 'AlmanacBindingTableView' => 'AphrontView', - 'AlmanacBindingTransaction' => 'AlmanacTransaction', + 'AlmanacBindingTransaction' => 'AlmanacModularTransaction', 'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'AlmanacBindingTransactionType' => 'AlmanacTransactionType', 'AlmanacBindingViewController' => 'AlmanacServiceController', 'AlmanacBindingsSearchEngineAttachment' => 'AlmanacSearchEngineAttachment', 'AlmanacCacheEngineExtension' => 'PhabricatorCacheEngineExtension', @@ -5189,6 +5251,8 @@ phutil_register_library_map(array( 'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCustomServiceType' => 'AlmanacServiceType', 'AlmanacDAO' => 'PhabricatorLiskDAO', + 'AlmanacDeletePropertyEditField' => 'PhabricatorEditField', + 'AlmanacDeletePropertyEditType' => 'PhabricatorEditType', 'AlmanacDevice' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', @@ -5202,19 +5266,23 @@ phutil_register_library_map(array( 'PhabricatorExtendedPolicyInterface', ), 'AlmanacDeviceController' => 'AlmanacController', + 'AlmanacDeviceDeletePropertyTransaction' => 'AlmanacDeviceTransactionType', 'AlmanacDeviceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'AlmanacDeviceEditController' => 'AlmanacDeviceController', 'AlmanacDeviceEditEngine' => 'PhabricatorEditEngine', 'AlmanacDeviceEditor' => 'AlmanacEditor', 'AlmanacDeviceListController' => 'AlmanacDeviceController', 'AlmanacDeviceNameNgrams' => 'PhabricatorSearchNgrams', + 'AlmanacDeviceNameTransaction' => 'AlmanacDeviceTransactionType', 'AlmanacDevicePHIDType' => 'PhabricatorPHIDType', 'AlmanacDevicePropertyEditEngine' => 'AlmanacPropertyEditEngine', 'AlmanacDeviceQuery' => 'AlmanacQuery', 'AlmanacDeviceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'AlmanacDeviceSearchEngine' => 'PhabricatorApplicationSearchEngine', - 'AlmanacDeviceTransaction' => 'AlmanacTransaction', + 'AlmanacDeviceSetPropertyTransaction' => 'AlmanacDeviceTransactionType', + 'AlmanacDeviceTransaction' => 'AlmanacModularTransaction', 'AlmanacDeviceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'AlmanacDeviceTransactionType' => 'AlmanacTransactionType', 'AlmanacDeviceViewController' => 'AlmanacDeviceController', 'AlmanacDrydockPoolServiceType' => 'AlmanacServiceType', 'AlmanacEditor' => 'PhabricatorApplicationTransactionEditor', @@ -5223,19 +5291,34 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorExtendedPolicyInterface', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorConduitResultInterface', ), + 'AlmanacInterfaceAddressTransaction' => 'AlmanacInterfaceTransactionType', 'AlmanacInterfaceDatasource' => 'PhabricatorTypeaheadDatasource', 'AlmanacInterfaceDeleteController' => 'AlmanacDeviceController', + 'AlmanacInterfaceDestroyTransaction' => 'AlmanacInterfaceTransactionType', + 'AlmanacInterfaceDeviceTransaction' => 'AlmanacInterfaceTransactionType', + 'AlmanacInterfaceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'AlmanacInterfaceEditController' => 'AlmanacDeviceController', + 'AlmanacInterfaceEditEngine' => 'PhabricatorEditEngine', + 'AlmanacInterfaceEditor' => 'AlmanacEditor', + 'AlmanacInterfaceNetworkTransaction' => 'AlmanacInterfaceTransactionType', 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', + 'AlmanacInterfacePortTransaction' => 'AlmanacInterfaceTransactionType', 'AlmanacInterfaceQuery' => 'AlmanacQuery', + 'AlmanacInterfaceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', + 'AlmanacInterfaceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacInterfaceTableView' => 'AphrontView', + 'AlmanacInterfaceTransaction' => 'AlmanacModularTransaction', + 'AlmanacInterfaceTransactionType' => 'AlmanacTransactionType', 'AlmanacKeys' => 'Phobject', 'AlmanacManageClusterServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', + 'AlmanacModularTransaction' => 'PhabricatorModularTransaction', 'AlmanacNames' => 'Phobject', 'AlmanacNamesTestCase' => 'PhabricatorTestCase', 'AlmanacNamespace' => array( @@ -5246,18 +5329,23 @@ phutil_register_library_map(array( 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', + 'PhabricatorConduitResultInterface', ), 'AlmanacNamespaceController' => 'AlmanacController', + 'AlmanacNamespaceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'AlmanacNamespaceEditController' => 'AlmanacNamespaceController', 'AlmanacNamespaceEditEngine' => 'PhabricatorEditEngine', - 'AlmanacNamespaceEditor' => 'PhabricatorApplicationTransactionEditor', + 'AlmanacNamespaceEditor' => 'AlmanacEditor', 'AlmanacNamespaceListController' => 'AlmanacNamespaceController', 'AlmanacNamespaceNameNgrams' => 'PhabricatorSearchNgrams', + 'AlmanacNamespaceNameTransaction' => 'AlmanacNamespaceTransactionType', 'AlmanacNamespacePHIDType' => 'PhabricatorPHIDType', 'AlmanacNamespaceQuery' => 'AlmanacQuery', + 'AlmanacNamespaceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'AlmanacNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine', - 'AlmanacNamespaceTransaction' => 'PhabricatorApplicationTransaction', + 'AlmanacNamespaceTransaction' => 'AlmanacModularTransaction', 'AlmanacNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'AlmanacNamespaceTransactionType' => 'AlmanacTransactionType', 'AlmanacNamespaceViewController' => 'AlmanacNamespaceController', 'AlmanacNetwork' => array( 'AlmanacDAO', @@ -5265,20 +5353,26 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', + 'PhabricatorConduitResultInterface', ), 'AlmanacNetworkController' => 'AlmanacController', + 'AlmanacNetworkEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'AlmanacNetworkEditController' => 'AlmanacNetworkController', 'AlmanacNetworkEditEngine' => 'PhabricatorEditEngine', - 'AlmanacNetworkEditor' => 'PhabricatorApplicationTransactionEditor', + 'AlmanacNetworkEditor' => 'AlmanacEditor', 'AlmanacNetworkListController' => 'AlmanacNetworkController', 'AlmanacNetworkNameNgrams' => 'PhabricatorSearchNgrams', + 'AlmanacNetworkNameTransaction' => 'AlmanacNetworkTransactionType', 'AlmanacNetworkPHIDType' => 'PhabricatorPHIDType', 'AlmanacNetworkQuery' => 'AlmanacQuery', + 'AlmanacNetworkSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'AlmanacNetworkSearchEngine' => 'PhabricatorApplicationSearchEngine', - 'AlmanacNetworkTransaction' => 'PhabricatorApplicationTransaction', + 'AlmanacNetworkTransaction' => 'AlmanacModularTransaction', 'AlmanacNetworkTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'AlmanacNetworkTransactionType' => 'AlmanacTransactionType', 'AlmanacNetworkViewController' => 'AlmanacNetworkController', 'AlmanacPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', + 'AlmanacPropertiesEditEngineExtension' => 'PhabricatorEditEngineExtension', 'AlmanacPropertiesSearchEngineAttachment' => 'AlmanacSearchEngineAttachment', 'AlmanacProperty' => array( 'AlmanacDAO', @@ -5305,23 +5399,31 @@ phutil_register_library_map(array( ), 'AlmanacServiceController' => 'AlmanacController', 'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource', + 'AlmanacServiceDeletePropertyTransaction' => 'AlmanacServiceTransactionType', + 'AlmanacServiceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'AlmanacServiceEditController' => 'AlmanacServiceController', 'AlmanacServiceEditEngine' => 'PhabricatorEditEngine', 'AlmanacServiceEditor' => 'AlmanacEditor', 'AlmanacServiceListController' => 'AlmanacServiceController', 'AlmanacServiceNameNgrams' => 'PhabricatorSearchNgrams', + 'AlmanacServiceNameTransaction' => 'AlmanacServiceTransactionType', 'AlmanacServicePHIDType' => 'PhabricatorPHIDType', 'AlmanacServicePropertyEditEngine' => 'AlmanacPropertyEditEngine', 'AlmanacServiceQuery' => 'AlmanacQuery', 'AlmanacServiceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'AlmanacServiceSearchEngine' => 'PhabricatorApplicationSearchEngine', - 'AlmanacServiceTransaction' => 'AlmanacTransaction', + 'AlmanacServiceSetPropertyTransaction' => 'AlmanacServiceTransactionType', + 'AlmanacServiceTransaction' => 'AlmanacModularTransaction', 'AlmanacServiceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'AlmanacServiceTransactionType' => 'AlmanacTransactionType', 'AlmanacServiceType' => 'Phobject', 'AlmanacServiceTypeDatasource' => 'PhabricatorTypeaheadDatasource', 'AlmanacServiceTypeTestCase' => 'PhabricatorTestCase', + 'AlmanacServiceTypeTransaction' => 'AlmanacServiceTransactionType', 'AlmanacServiceViewController' => 'AlmanacServiceController', - 'AlmanacTransaction' => 'PhabricatorApplicationTransaction', + 'AlmanacSetPropertyEditField' => 'PhabricatorEditField', + 'AlmanacSetPropertyEditType' => 'PhabricatorEditType', + 'AlmanacTransactionType' => 'PhabricatorModularTransactionType', 'AphlictDropdownDataQuery' => 'Phobject', 'Aphront304Response' => 'AphrontResponse', 'Aphront400Response' => 'AphrontResponse', @@ -5885,6 +5987,7 @@ phutil_register_library_map(array( 'DiffusionAuditorsAddSelfHeraldAction' => 'DiffusionAuditorsHeraldAction', 'DiffusionAuditorsHeraldAction' => 'HeraldAction', 'DiffusionBlameConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', + 'DiffusionBlameController' => 'DiffusionController', 'DiffusionBlameQuery' => 'DiffusionQuery', 'DiffusionBlockHeraldAction' => 'HeraldAction', 'DiffusionBranchListView' => 'DiffusionView', @@ -5985,6 +6088,8 @@ phutil_register_library_map(array( 'DiffusionDiffController' => 'DiffusionController', 'DiffusionDiffInlineCommentQuery' => 'PhabricatorDiffInlineCommentQuery', 'DiffusionDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', + 'DiffusionDocumentController' => 'DiffusionController', + 'DiffusionDocumentRenderingEngine' => 'PhabricatorDocumentRenderingEngine', 'DiffusionDoorkeeperCommitFeedStoryPublisher' => 'DoorkeeperFeedStoryPublisher', 'DiffusionEmptyResultView' => 'DiffusionView', 'DiffusionExistsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', @@ -7525,6 +7630,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionWarningException' => 'Exception', 'PhabricatorApplicationTransactionWarningResponse' => 'AphrontProxyResponse', 'PhabricatorApplicationUninstallController' => 'PhabricatorApplicationsController', + 'PhabricatorApplicationUninstallTransaction' => 'PhabricatorApplicationTransactionType', 'PhabricatorApplicationsApplication' => 'PhabricatorApplication', 'PhabricatorApplicationsController' => 'PhabricatorController', 'PhabricatorApplicationsListController' => 'PhabricatorApplicationsController', @@ -8401,6 +8507,7 @@ phutil_register_library_map(array( 'PhabricatorDivinerApplication' => 'PhabricatorApplication', 'PhabricatorDocumentEngine' => 'Phobject', 'PhabricatorDocumentRef' => 'Phobject', + 'PhabricatorDocumentRenderingEngine' => 'Phobject', 'PhabricatorDoorkeeperApplication' => 'PhabricatorApplication', 'PhabricatorDraft' => 'PhabricatorDraftDAO', 'PhabricatorDraftDAO' => 'PhabricatorLiskDAO', @@ -8624,6 +8731,7 @@ phutil_register_library_map(array( 'PhabricatorFileDeleteController' => 'PhabricatorFileController', 'PhabricatorFileDeleteTransaction' => 'PhabricatorFileTransactionType', 'PhabricatorFileDocumentController' => 'PhabricatorFileController', + 'PhabricatorFileDocumentRenderingEngine' => 'PhabricatorDocumentRenderingEngine', 'PhabricatorFileDropUploadController' => 'PhabricatorFileController', 'PhabricatorFileEditController' => 'PhabricatorFileController', 'PhabricatorFileEditEngine' => 'PhabricatorEditEngine', @@ -8816,6 +8924,7 @@ phutil_register_library_map(array( 'PhabricatorInlineSummaryView' => 'AphrontView', 'PhabricatorInstructionsEditField' => 'PhabricatorEditField', 'PhabricatorIntConfigType' => 'PhabricatorTextConfigType', + 'PhabricatorIntEditField' => 'PhabricatorEditField', 'PhabricatorIntExportField' => 'PhabricatorExportField', 'PhabricatorInternalSetting' => 'PhabricatorSetting', 'PhabricatorInternationalizationManagementExtractWorkflow' => 'PhabricatorInternationalizationManagementWorkflow', diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index c1dc0d1305..78e6dac9d3 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -61,6 +61,10 @@ final class AphrontRequest extends Phobject { */ public function getURILineRange($key, $limit) { $range = $this->getURIData($key); + return self::parseURILineRange($range, $limit); + } + + public static function parseURILineRange($range, $limit) { if (!strlen($range)) { return null; } diff --git a/src/applications/almanac/conduit/AlmanacBindingEditConduitAPIMethod.php b/src/applications/almanac/conduit/AlmanacBindingEditConduitAPIMethod.php new file mode 100644 index 0000000000..e36d9e0ded --- /dev/null +++ b/src/applications/almanac/conduit/AlmanacBindingEditConduitAPIMethod.php @@ -0,0 +1,19 @@ +isFormPost()) { - $type_disable = AlmanacBindingTransaction::TYPE_DISABLE; + $type_disable = AlmanacBindingDisableTransaction::TRANSACTIONTYPE; $xactions = array(); diff --git a/src/applications/almanac/controller/AlmanacBindingEditController.php b/src/applications/almanac/controller/AlmanacBindingEditController.php index 5f1fe30043..f33bf73ccb 100644 --- a/src/applications/almanac/controller/AlmanacBindingEditController.php +++ b/src/applications/almanac/controller/AlmanacBindingEditController.php @@ -58,7 +58,7 @@ final class AlmanacBindingEditController if ($request->isFormPost()) { $v_interface = $request->getArr('interfacePHIDs'); - $type_interface = AlmanacBindingTransaction::TYPE_INTERFACE; + $type_interface = AlmanacBindingInterfaceTransaction::TRANSACTIONTYPE; $xactions = array(); diff --git a/src/applications/almanac/controller/AlmanacController.php b/src/applications/almanac/controller/AlmanacController.php index d6d7188511..24a8986766 100644 --- a/src/applications/almanac/controller/AlmanacController.php +++ b/src/applications/almanac/controller/AlmanacController.php @@ -38,7 +38,7 @@ abstract class AlmanacController )); $builtins = $object->getAlmanacPropertyFieldSpecifications(); - $defaults = mpull($builtins, null, 'getValueForTransaction'); + $defaults = mpull($builtins, 'getValueForTransaction'); // Sort fields so builtin fields appear first, then fields are ordered // alphabetically. @@ -65,6 +65,7 @@ abstract class AlmanacController $value = $property->getFieldValue(); $is_builtin = isset($builtins[$key]); + $is_persistent = (bool)$property->getID(); $delete_uri = id(new PhutilURI($delete_base)) ->setQueryParams( @@ -83,7 +84,7 @@ abstract class AlmanacController $delete = javelin_tag( 'a', array( - 'class' => ($can_edit + 'class' => (($can_edit && $is_persistent) ? 'button button-grey small' : 'button button-grey small disabled'), 'sigil' => 'workflow', diff --git a/src/applications/almanac/controller/AlmanacInterfaceDeleteController.php b/src/applications/almanac/controller/AlmanacInterfaceDeleteController.php index ddbe979af6..4d40031b93 100644 --- a/src/applications/almanac/controller/AlmanacInterfaceDeleteController.php +++ b/src/applications/almanac/controller/AlmanacInterfaceDeleteController.php @@ -34,26 +34,21 @@ final class AlmanacInterfaceDeleteController } if ($request->isFormPost()) { - $type_interface = AlmanacDeviceTransaction::TYPE_INTERFACE; + $type_destroy = AlmanacInterfaceDestroyTransaction::TRANSACTIONTYPE; $xactions = array(); - $v_old = array( - 'id' => $interface->getID(), - ) + $interface->toAddress()->toDictionary(); + $xactions[] = $interface->getApplicationTransactionTemplate() + ->setTransactionType($type_destroy) + ->setNewValue(true); - $xactions[] = id(new AlmanacDeviceTransaction()) - ->setTransactionType($type_interface) - ->setOldValue($v_old) - ->setNewValue(null); - - $editor = id(new AlmanacDeviceEditor()) + $editor = id(new AlmanacInterfaceEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); - $editor->applyTransactions($device, $xactions); + $editor->applyTransactions($interface, $xactions); return id(new AphrontRedirectResponse())->setURI($device_uri); } diff --git a/src/applications/almanac/controller/AlmanacInterfaceEditController.php b/src/applications/almanac/controller/AlmanacInterfaceEditController.php index d223360003..017dad00ce 100644 --- a/src/applications/almanac/controller/AlmanacInterfaceEditController.php +++ b/src/applications/almanac/controller/AlmanacInterfaceEditController.php @@ -4,32 +4,16 @@ final class AlmanacInterfaceEditController extends AlmanacDeviceController { public function handleRequest(AphrontRequest $request) { - $viewer = $request->getViewer(); + $viewer = $this->getViewer(); + + $engine = id(new AlmanacInterfaceEditEngine()) + ->setController($this); $id = $request->getURIData('id'); - if ($id) { - $interface = id(new AlmanacInterfaceQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$interface) { - return new Aphront404Response(); - } - - $device = $interface->getDevice(); - - $is_new = false; - $title = pht('Edit Interface'); - $save_button = pht('Save Changes'); - } else { + if (!$id) { $device = id(new AlmanacDeviceQuery()) ->setViewer($viewer) - ->withIDs(array($request->getStr('deviceID'))) + ->withIDs(array($request->getInt('deviceID'))) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, @@ -40,127 +24,12 @@ final class AlmanacInterfaceEditController return new Aphront404Response(); } - $interface = AlmanacInterface::initializeNewInterface(); - $is_new = true; - - $title = pht('Create Interface'); - $save_button = pht('Create Interface'); + $engine + ->addContextParameter('deviceID', $device->getID()) + ->setDevice($device); } - $device_uri = $device->getURI(); - $cancel_uri = $device_uri; - - $v_network = $interface->getNetworkPHID(); - - $v_address = $interface->getAddress(); - $e_address = true; - - $v_port = $interface->getPort(); - - $validation_exception = null; - - if ($request->isFormPost()) { - $v_network = $request->getStr('networkPHID'); - $v_address = $request->getStr('address'); - $v_port = $request->getStr('port'); - - $type_interface = AlmanacDeviceTransaction::TYPE_INTERFACE; - - $address = AlmanacAddress::newFromParts($v_network, $v_address, $v_port); - - $xaction = id(new AlmanacDeviceTransaction()) - ->setTransactionType($type_interface) - ->setNewValue($address->toDictionary()); - - if ($interface->getID()) { - $xaction->setOldValue(array( - 'id' => $interface->getID(), - ) + $interface->toAddress()->toDictionary()); - } else { - $xaction->setOldValue(array()); - } - - $xactions = array(); - $xactions[] = $xaction; - - $editor = id(new AlmanacDeviceEditor()) - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true); - - try { - $editor->applyTransactions($device, $xactions); - - $device_uri = $device->getURI(); - return id(new AphrontRedirectResponse())->setURI($device_uri); - } catch (PhabricatorApplicationTransactionValidationException $ex) { - $validation_exception = $ex; - $e_address = $ex->getShortMessage($type_interface); - } - } - - $networks = id(new AlmanacNetworkQuery()) - ->setViewer($viewer) - ->execute(); - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setLabel(pht('Network')) - ->setName('networkPHID') - ->setValue($v_network) - ->setOptions(mpull($networks, 'getName', 'getPHID'))) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('Address')) - ->setName('address') - ->setValue($v_address) - ->setError($e_address)) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('Port')) - ->setName('port') - ->setValue($v_port) - ->setError($e_address)) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->addCancelButton($cancel_uri) - ->setValue($save_button)); - - $box = id(new PHUIObjectBoxView()) - ->setValidationException($validation_exception) - ->setHeaderText(pht('Interface')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setForm($form); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($device->getName(), $device_uri); - if ($is_new) { - $crumbs->addTextCrumb(pht('Create Interface')); - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Create Interface')) - ->setHeaderIcon('fa-plus-square'); - } else { - $crumbs->addTextCrumb(pht('Edit Interface')); - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Edit Interface')) - ->setHeaderIcon('fa-pencil'); - } - $crumbs->setBorder(true); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setFooter(array( - $box, - )); - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->appendChild($view); - + return $engine->buildResponse(); } } diff --git a/src/applications/almanac/controller/AlmanacPropertyDeleteController.php b/src/applications/almanac/controller/AlmanacPropertyDeleteController.php index 93ec4ced64..f38670041b 100644 --- a/src/applications/almanac/controller/AlmanacPropertyDeleteController.php +++ b/src/applications/almanac/controller/AlmanacPropertyDeleteController.php @@ -39,8 +39,10 @@ final class AlmanacPropertyDeleteController $validation_exception = null; if ($request->isFormPost()) { + $xaction_type = $object->getAlmanacPropertyDeleteTransactionType(); + $xaction = $object->getApplicationTransactionTemplate() - ->setTransactionType(AlmanacTransaction::TYPE_PROPERTY_REMOVE) + ->setTransactionType($xaction_type) ->setMetadataValue('almanac.property', $key); $editor = $object->getApplicationTransactionEditor() diff --git a/src/applications/almanac/editor/AlmanacBindingEditEngine.php b/src/applications/almanac/editor/AlmanacBindingEditEngine.php new file mode 100644 index 0000000000..5146578fff --- /dev/null +++ b/src/applications/almanac/editor/AlmanacBindingEditEngine.php @@ -0,0 +1,172 @@ +service = $service; + return $this; + } + + public function getService() { + if (!$this->service) { + throw new PhutilInvalidStateException('setService'); + } + return $this->service; + } + + public function isEngineConfigurable() { + return false; + } + + public function getEngineName() { + return pht('Almanac Bindings'); + } + + public function getSummaryHeader() { + return pht('Edit Almanac Binding Configurations'); + } + + public function getSummaryText() { + return pht('This engine is used to edit Almanac bindings.'); + } + + public function getEngineApplicationClass() { + return 'PhabricatorAlmanacApplication'; + } + + protected function newEditableObject() { + $service = $this->getService(); + return AlmanacBinding::initializeNewBinding($service); + } + + protected function newEditableObjectForDocumentation() { + $service_type = AlmanacCustomServiceType::SERVICETYPE; + $service = AlmanacService::initializeNewService($service_type); + $this->setService($service); + return $this->newEditableObject(); + } + + protected function newEditableObjectFromConduit(array $raw_xactions) { + $service_phid = null; + foreach ($raw_xactions as $raw_xaction) { + if ($raw_xaction['type'] !== 'service') { + continue; + } + + $service_phid = $raw_xaction['value']; + } + + if ($service_phid === null) { + throw new Exception( + pht( + 'When creating a new Almanac binding via the Conduit API, you '. + 'must provide a "service" transaction to select a service to bind.')); + } + + $service = id(new AlmanacServiceQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs(array($service_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$service) { + throw new Exception( + pht( + 'Service "%s" is unrecognized, restricted, or you do not have '. + 'permission to edit it.', + $service_phid)); + } + + $this->setService($service); + + return $this->newEditableObject(); + } + + protected function newObjectQuery() { + return id(new AlmanacBindingQuery()) + ->needProperties(true); + } + + protected function getObjectCreateTitleText($object) { + return pht('Create Binding'); + } + + protected function getObjectCreateButtonText($object) { + return pht('Create Binding'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit Binding'); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Binding'); + } + + protected function getObjectCreateShortText() { + return pht('Create Binding'); + } + + protected function getObjectName() { + return pht('Binding'); + } + + protected function getEditorURI() { + return '/almanac/binding/edit/'; + } + + protected function getObjectCreateCancelURI($object) { + return '/almanac/binding/'; + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function buildCustomEditFields($object) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('service') + ->setLabel(pht('Service')) + ->setIsConduitOnly(true) + ->setTransactionType( + AlmanacBindingServiceTransaction::TRANSACTIONTYPE) + ->setDescription(pht('Service to create a binding for.')) + ->setConduitDescription(pht('Select the service to bind.')) + ->setConduitTypeDescription(pht('Service PHID.')) + ->setValue($object->getServicePHID()), + id(new PhabricatorTextEditField()) + ->setKey('interface') + ->setLabel(pht('Interface')) + ->setIsConduitOnly(true) + ->setTransactionType( + AlmanacBindingInterfaceTransaction::TRANSACTIONTYPE) + ->setDescription(pht('Interface to bind the service to.')) + ->setConduitDescription(pht('Set the interface to bind.')) + ->setConduitTypeDescription(pht('Interface PHID.')) + ->setValue($object->getInterfacePHID()), + id(new PhabricatorBoolEditField()) + ->setKey('disabled') + ->setLabel(pht('Disabled')) + ->setIsConduitOnly(true) + ->setTransactionType( + AlmanacBindingDisableTransaction::TRANSACTIONTYPE) + ->setDescription(pht('Disable or enable the binding.')) + ->setConduitDescription(pht('Disable or enable the binding.')) + ->setConduitTypeDescription(pht('True to disable the binding.')) + ->setValue($object->getIsDisabled()) + ->setOptions( + pht('Enable Binding'), + pht('Disable Binding')), + ); + } + +} diff --git a/src/applications/almanac/editor/AlmanacBindingEditor.php b/src/applications/almanac/editor/AlmanacBindingEditor.php index 6532bf671f..f0d164317a 100644 --- a/src/applications/almanac/editor/AlmanacBindingEditor.php +++ b/src/applications/almanac/editor/AlmanacBindingEditor.php @@ -3,173 +3,20 @@ final class AlmanacBindingEditor extends AlmanacEditor { - private $devicePHID; - public function getEditorObjectsDescription() { return pht('Almanac Binding'); } - public function getTransactionTypes() { - $types = parent::getTransactionTypes(); - - $types[] = AlmanacBindingTransaction::TYPE_INTERFACE; - $types[] = AlmanacBindingTransaction::TYPE_DISABLE; - - return $types; + public function getCreateObjectTitle($author, $object) { + return pht('%s created this binding.', $author); } - protected function getCustomTransactionOldValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { - case AlmanacBindingTransaction::TYPE_INTERFACE: - return $object->getInterfacePHID(); - case AlmanacBindingTransaction::TYPE_DISABLE: - return $object->getIsDisabled(); - } - - return parent::getCustomTransactionOldValue($object, $xaction); + public function getCreateObjectTitleForFeed($author, $object) { + return pht('%s created %s.', $author, $object); } - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacBindingTransaction::TYPE_INTERFACE: - return $xaction->getNewValue(); - case AlmanacBindingTransaction::TYPE_DISABLE: - return (int)$xaction->getNewValue(); - } - - return parent::getCustomTransactionNewValue($object, $xaction); + protected function supportsSearch() { + return true; } - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacBindingTransaction::TYPE_INTERFACE: - $interface = id(new AlmanacInterfaceQuery()) - ->setViewer($this->requireActor()) - ->withPHIDs(array($xaction->getNewValue())) - ->executeOne(); - $object->setDevicePHID($interface->getDevicePHID()); - $object->setInterfacePHID($interface->getPHID()); - return; - case AlmanacBindingTransaction::TYPE_DISABLE: - $object->setIsDisabled($xaction->getNewValue()); - return; - } - - return parent::applyCustomInternalTransaction($object, $xaction); - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacBindingTransaction::TYPE_DISABLE: - return; - case AlmanacBindingTransaction::TYPE_INTERFACE: - $interface_phids = array(); - - $interface_phids[] = $xaction->getOldValue(); - $interface_phids[] = $xaction->getNewValue(); - - $interface_phids = array_filter($interface_phids); - $interface_phids = array_unique($interface_phids); - - $interfaces = id(new AlmanacInterfaceQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withPHIDs($interface_phids) - ->execute(); - - $device_phids = array(); - foreach ($interfaces as $interface) { - $device_phids[] = $interface->getDevicePHID(); - } - - $device_phids = array_unique($device_phids); - - $devices = id(new AlmanacDeviceQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withPHIDs($device_phids) - ->execute(); - - foreach ($devices as $device) { - $device->rebuildClusterBindingStatus(); - } - return; - } - - return parent::applyCustomExternalTransaction($object, $xaction); - } - - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - - switch ($type) { - case AlmanacBindingTransaction::TYPE_INTERFACE: - $missing = $this->validateIsEmptyTextField( - $object->getInterfacePHID(), - $xactions); - if ($missing) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht('Bindings must specify an interface.'), - nonempty(last($xactions), null)); - $error->setIsMissingFieldError(true); - $errors[] = $error; - } else if ($xactions) { - foreach ($xactions as $xaction) { - $interfaces = id(new AlmanacInterfaceQuery()) - ->setViewer($this->requireActor()) - ->withPHIDs(array($xaction->getNewValue())) - ->execute(); - if (!$interfaces) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'You can not bind a service to an invalid or restricted '. - 'interface.'), - $xaction); - $errors[] = $error; - } - } - - $final_value = last($xactions)->getNewValue(); - - $binding = id(new AlmanacBindingQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withServicePHIDs(array($object->getServicePHID())) - ->withInterfacePHIDs(array($final_value)) - ->executeOne(); - if ($binding && ($binding->getID() != $object->getID())) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Already Bound'), - pht( - 'You can not bind a service to the same interface multiple '. - 'times.'), - last($xactions)); - $errors[] = $error; - } - } - break; - } - - return $errors; - } - - - } diff --git a/src/applications/almanac/editor/AlmanacDeviceEditEngine.php b/src/applications/almanac/editor/AlmanacDeviceEditEngine.php index 1d325e403b..fa0625e6ed 100644 --- a/src/applications/almanac/editor/AlmanacDeviceEditEngine.php +++ b/src/applications/almanac/editor/AlmanacDeviceEditEngine.php @@ -30,7 +30,8 @@ final class AlmanacDeviceEditEngine } protected function newObjectQuery() { - return new AlmanacDeviceQuery(); + return id(new AlmanacDeviceQuery()) + ->needProperties(true); } protected function getObjectCreateTitleText($object) { @@ -80,7 +81,7 @@ final class AlmanacDeviceEditEngine ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the device.')) - ->setTransactionType(AlmanacDeviceTransaction::TYPE_NAME) + ->setTransactionType(AlmanacDeviceNameTransaction::TRANSACTIONTYPE) ->setIsRequired(true) ->setValue($object->getName()), ); diff --git a/src/applications/almanac/editor/AlmanacDeviceEditor.php b/src/applications/almanac/editor/AlmanacDeviceEditor.php index 8e110954a3..71032f1482 100644 --- a/src/applications/almanac/editor/AlmanacDeviceEditor.php +++ b/src/applications/almanac/editor/AlmanacDeviceEditor.php @@ -7,329 +7,25 @@ final class AlmanacDeviceEditor return pht('Almanac Device'); } + public function getCreateObjectTitle($author, $object) { + return pht('%s created this device.', $author); + } + + public function getCreateObjectTitleForFeed($author, $object) { + return pht('%s created %s.', $author, $object); + } + + protected function supportsSearch() { + return true; + } + public function getTransactionTypes() { $types = parent::getTransactionTypes(); - $types[] = AlmanacDeviceTransaction::TYPE_NAME; - $types[] = AlmanacDeviceTransaction::TYPE_INTERFACE; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; return $types; } - protected function getCustomTransactionOldValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { - case AlmanacDeviceTransaction::TYPE_NAME: - return $object->getName(); - } - - return parent::getCustomTransactionOldValue($object, $xaction); - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacDeviceTransaction::TYPE_NAME: - case AlmanacDeviceTransaction::TYPE_INTERFACE: - return $xaction->getNewValue(); - } - - return parent::getCustomTransactionNewValue($object, $xaction); - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacDeviceTransaction::TYPE_NAME: - $object->setName($xaction->getNewValue()); - return; - case AlmanacDeviceTransaction::TYPE_INTERFACE: - return; - } - - return parent::applyCustomInternalTransaction($object, $xaction); - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacDeviceTransaction::TYPE_NAME: - return; - case AlmanacDeviceTransaction::TYPE_INTERFACE: - $old = $xaction->getOldValue(); - if ($old) { - $interface = id(new AlmanacInterfaceQuery()) - ->setViewer($this->requireActor()) - ->withIDs(array($old['id'])) - ->executeOne(); - if (!$interface) { - throw new Exception(pht('Unable to load interface!')); - } - } else { - $interface = AlmanacInterface::initializeNewInterface() - ->setDevicePHID($object->getPHID()); - } - - $new = $xaction->getNewValue(); - if ($new) { - $interface - ->setNetworkPHID($new['networkPHID']) - ->setAddress($new['address']) - ->setPort((int)$new['port']); - - if (idx($new, 'phid')) { - $interface->setPHID($new['phid']); - } - - $interface->save(); - } else { - $interface->delete(); - } - return; - } - - return parent::applyCustomExternalTransaction($object, $xaction); - } - - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - - switch ($type) { - case AlmanacDeviceTransaction::TYPE_NAME: - $missing = $this->validateIsEmptyTextField( - $object->getName(), - $xactions); - - if ($missing) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht('Device name is required.'), - nonempty(last($xactions), null)); - - $error->setIsMissingFieldError(true); - $errors[] = $error; - } else { - foreach ($xactions as $xaction) { - $message = null; - $name = $xaction->getNewValue(); - - try { - AlmanacNames::validateName($name); - } catch (Exception $ex) { - $message = $ex->getMessage(); - } - - if ($message !== null) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - $message, - $xaction); - $errors[] = $error; - continue; - } - - $other = id(new AlmanacDeviceQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withNames(array($name)) - ->executeOne(); - if ($other && ($other->getID() != $object->getID())) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Not Unique'), - pht('Almanac devices must have unique names.'), - $xaction); - $errors[] = $error; - continue; - } - - if ($name === $object->getName()) { - continue; - } - - $namespace = AlmanacNamespace::loadRestrictedNamespace( - $this->getActor(), - $name); - if ($namespace) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Restricted'), - pht( - 'You do not have permission to create Almanac devices '. - 'within the "%s" namespace.', - $namespace->getName()), - $xaction); - $errors[] = $error; - continue; - } - } - } - - break; - case AlmanacDeviceTransaction::TYPE_INTERFACE: - // We want to make sure that all the affected networks are visible to - // the actor, any edited interfaces exist, and that the actual address - // components are valid. - - $network_phids = array(); - foreach ($xactions as $xaction) { - $old = $xaction->getOldValue(); - $new = $xaction->getNewValue(); - if ($old) { - $network_phids[] = $old['networkPHID']; - } - if ($new) { - $network_phids[] = $new['networkPHID']; - - $address = $new['address']; - if (!strlen($address)) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht('Interfaces must have an address.'), - $xaction); - $errors[] = $error; - } else { - // TODO: Validate addresses, but IPv6 addresses are not trivial - // to validate. - } - - $port = $new['port']; - if (!strlen($port)) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht('Interfaces must have a port.'), - $xaction); - $errors[] = $error; - } else if ((int)$port < 1 || (int)$port > 65535) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Port numbers must be between 1 and 65535, inclusive.'), - $xaction); - $errors[] = $error; - } - - $phid = idx($new, 'phid'); - if ($phid) { - $interface_phid_type = AlmanacInterfacePHIDType::TYPECONST; - if (phid_get_type($phid) !== $interface_phid_type) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Precomputed interface PHIDs must be of type '. - 'AlmanacInterfacePHIDType.'), - $xaction); - $errors[] = $error; - } - } - } - } - - if ($network_phids) { - $networks = id(new AlmanacNetworkQuery()) - ->setViewer($this->requireActor()) - ->withPHIDs($network_phids) - ->execute(); - $networks = mpull($networks, null, 'getPHID'); - } else { - $networks = array(); - } - - $addresses = array(); - foreach ($xactions as $xaction) { - $old = $xaction->getOldValue(); - if ($old) { - $network = idx($networks, $old['networkPHID']); - if (!$network) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'You can not edit an interface which belongs to a '. - 'nonexistent or restricted network.'), - $xaction); - $errors[] = $error; - } - - $addresses[] = $old['id']; - } - - $new = $xaction->getNewValue(); - if ($new) { - $network = idx($networks, $new['networkPHID']); - if (!$network) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'You can not add an interface on a nonexistent or '. - 'restricted network.'), - $xaction); - $errors[] = $error; - } - } - } - - if ($addresses) { - $interfaces = id(new AlmanacInterfaceQuery()) - ->setViewer($this->requireActor()) - ->withDevicePHIDs(array($object->getPHID())) - ->withIDs($addresses) - ->execute(); - $interfaces = mpull($interfaces, null, 'getID'); - } else { - $interfaces = array(); - } - - foreach ($xactions as $xaction) { - $old = $xaction->getOldValue(); - if ($old) { - $interface = idx($interfaces, $old['id']); - if (!$interface) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht('You can not edit an invalid or restricted interface.'), - $xaction); - $errors[] = $error; - continue; - } - - $new = $xaction->getNewValue(); - if (!$new) { - if ($interface->loadIsInUse()) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('In Use'), - pht('You can not delete an interface which is still in use.'), - $xaction); - $errors[] = $error; - } - } - } - } - break; - } - - return $errors; - } - } diff --git a/src/applications/almanac/editor/AlmanacEditor.php b/src/applications/almanac/editor/AlmanacEditor.php index a628606dbc..54c32c79cd 100644 --- a/src/applications/almanac/editor/AlmanacEditor.php +++ b/src/applications/almanac/editor/AlmanacEditor.php @@ -7,150 +7,4 @@ abstract class AlmanacEditor return 'PhabricatorAlmanacApplication'; } - protected function supportsSearch() { - return true; - } - - public function getTransactionTypes() { - $types = parent::getTransactionTypes(); - - $types[] = AlmanacTransaction::TYPE_PROPERTY_UPDATE; - $types[] = AlmanacTransaction::TYPE_PROPERTY_REMOVE; - - return $types; - } - - protected function getCustomTransactionOldValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { - case AlmanacTransaction::TYPE_PROPERTY_UPDATE: - case AlmanacTransaction::TYPE_PROPERTY_REMOVE: - $property_key = $xaction->getMetadataValue('almanac.property'); - $exists = $object->hasAlmanacProperty($property_key); - $value = $object->getAlmanacPropertyValue($property_key); - return array( - 'existed' => $exists, - 'value' => $value, - ); - } - - return parent::getCustomTransactionOldValue($object, $xaction); - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacTransaction::TYPE_PROPERTY_UPDATE: - case AlmanacTransaction::TYPE_PROPERTY_REMOVE: - return $xaction->getNewValue(); - } - - return parent::getCustomTransactionNewValue($object, $xaction); - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacTransaction::TYPE_PROPERTY_UPDATE: - case AlmanacTransaction::TYPE_PROPERTY_REMOVE: - return; - } - - return parent::applyCustomInternalTransaction($object, $xaction); - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacTransaction::TYPE_PROPERTY_UPDATE: - $property_key = $xaction->getMetadataValue('almanac.property'); - if ($object->hasAlmanacProperty($property_key)) { - $property = $object->getAlmanacProperty($property_key); - } else { - $property = id(new AlmanacProperty()) - ->setObjectPHID($object->getPHID()) - ->setFieldName($property_key); - } - $property - ->setFieldValue($xaction->getNewValue()) - ->save(); - return; - case AlmanacTransaction::TYPE_PROPERTY_REMOVE: - $property_key = $xaction->getMetadataValue('almanac.property'); - if ($object->hasAlmanacProperty($property_key)) { - $property = $object->getAlmanacProperty($property_key); - $property->delete(); - } - return; - } - - return parent::applyCustomExternalTransaction($object, $xaction); - } - - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - - switch ($type) { - case AlmanacTransaction::TYPE_PROPERTY_UPDATE: - foreach ($xactions as $xaction) { - $property_key = $xaction->getMetadataValue('almanac.property'); - - $message = null; - try { - AlmanacNames::validateName($property_key); - } catch (Exception $ex) { - $message = $ex->getMessage(); - } - - if ($message !== null) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - $message, - $xaction); - $errors[] = $error; - continue; - } - - $new_value = $xaction->getNewValue(); - try { - phutil_json_encode($new_value); - } catch (Exception $ex) { - $message = pht( - 'Almanac property values must be representable in JSON. %s', - $ex->getMessage()); - } - - if ($message !== null) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - $message, - $xaction); - $errors[] = $error; - continue; - } - } - break; - - case AlmanacTransaction::TYPE_PROPERTY_REMOVE: - // NOTE: No name validation on removals since it's OK to delete - // an invalid property that somehow came into existence. - break; - } - - return $errors; - } - } diff --git a/src/applications/almanac/editor/AlmanacInterfaceEditEngine.php b/src/applications/almanac/editor/AlmanacInterfaceEditEngine.php new file mode 100644 index 0000000000..2c68ed0374 --- /dev/null +++ b/src/applications/almanac/editor/AlmanacInterfaceEditEngine.php @@ -0,0 +1,183 @@ +device = $device; + return $this; + } + + public function getDevice() { + if (!$this->device) { + throw new PhutilInvalidStateException('setDevice'); + } + return $this->device; + } + + public function isEngineConfigurable() { + return false; + } + + public function getEngineName() { + return pht('Almanac Interfaces'); + } + + public function getSummaryHeader() { + return pht('Edit Almanac Interface Configurations'); + } + + public function getSummaryText() { + return pht('This engine is used to edit Almanac interfaces.'); + } + + public function getEngineApplicationClass() { + return 'PhabricatorAlmanacApplication'; + } + + protected function newEditableObject() { + $interface = AlmanacInterface::initializeNewInterface(); + + $device = $this->getDevice(); + $interface + ->setDevicePHID($device->getPHID()) + ->attachDevice($device); + + return $interface; + } + + protected function newEditableObjectForDocumentation() { + $this->setDevice(new AlmanacDevice()); + return $this->newEditableObject(); + } + + protected function newEditableObjectFromConduit(array $raw_xactions) { + $device_phid = null; + foreach ($raw_xactions as $raw_xaction) { + if ($raw_xaction['type'] !== 'device') { + continue; + } + + $device_phid = $raw_xaction['value']; + } + + if ($device_phid === null) { + throw new Exception( + pht( + 'When creating a new Almanac interface via the Conduit API, you '. + 'must provide a "device" transaction to select a device.')); + } + + $device = id(new AlmanacDeviceQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs(array($device_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$device) { + throw new Exception( + pht( + 'Device "%s" is unrecognized, restricted, or you do not have '. + 'permission to edit it.', + $device_phid)); + } + + $this->setDevice($device); + + return $this->newEditableObject(); + } + + protected function newObjectQuery() { + return new AlmanacInterfaceQuery(); + } + + protected function getObjectCreateTitleText($object) { + return pht('Create Interface'); + } + + protected function getObjectCreateButtonText($object) { + return pht('Create Interface'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit Interface'); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Interface'); + } + + protected function getObjectCreateShortText() { + return pht('Create Interface'); + } + + protected function getObjectName() { + return pht('Interface'); + } + + protected function getEditorURI() { + return '/almanac/interface/edit/'; + } + + protected function getObjectCreateCancelURI($object) { + return '/almanac/interface/'; + } + + protected function getObjectViewURI($object) { + return $object->getDevice()->getURI(); + } + + protected function buildCustomEditFields($object) { + $viewer = $this->getViewer(); + + // TODO: Some day, this should be a datasource. + $networks = id(new AlmanacNetworkQuery()) + ->setViewer($viewer) + ->execute(); + $network_map = mpull($networks, 'getName', 'getPHID'); + + return array( + id(new PhabricatorTextEditField()) + ->setKey('device') + ->setLabel(pht('Device')) + ->setIsConduitOnly(true) + ->setTransactionType( + AlmanacInterfaceDeviceTransaction::TRANSACTIONTYPE) + ->setDescription(pht('When creating an interface, set the device.')) + ->setConduitDescription(pht('Set the device.')) + ->setConduitTypeDescription(pht('Device PHID.')) + ->setValue($object->getDevicePHID()), + id(new PhabricatorSelectEditField()) + ->setKey('network') + ->setLabel(pht('Network')) + ->setDescription(pht('Network for the interface.')) + ->setTransactionType( + AlmanacInterfaceNetworkTransaction::TRANSACTIONTYPE) + ->setValue($object->getNetworkPHID()) + ->setOptions($network_map), + id(new PhabricatorTextEditField()) + ->setKey('address') + ->setLabel(pht('Address')) + ->setDescription(pht('Address of the service.')) + ->setTransactionType( + AlmanacInterfaceAddressTransaction::TRANSACTIONTYPE) + ->setIsRequired(true) + ->setValue($object->getAddress()), + id(new PhabricatorIntEditField()) + ->setKey('port') + ->setLabel(pht('Port')) + ->setDescription(pht('Port of the service.')) + ->setTransactionType(AlmanacInterfacePortTransaction::TRANSACTIONTYPE) + ->setIsRequired(true) + ->setValue($object->getPort()), + ); + } + +} diff --git a/src/applications/almanac/editor/AlmanacInterfaceEditor.php b/src/applications/almanac/editor/AlmanacInterfaceEditor.php new file mode 100644 index 0000000000..865402db36 --- /dev/null +++ b/src/applications/almanac/editor/AlmanacInterfaceEditor.php @@ -0,0 +1,18 @@ +setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the namespace.')) - ->setTransactionType(AlmanacNamespaceTransaction::TYPE_NAME) + ->setTransactionType(AlmanacNamespaceNameTransaction::TRANSACTIONTYPE) ->setIsRequired(true) ->setValue($object->getName()), ); diff --git a/src/applications/almanac/editor/AlmanacNamespaceEditor.php b/src/applications/almanac/editor/AlmanacNamespaceEditor.php index 5243d3c935..5da034b388 100644 --- a/src/applications/almanac/editor/AlmanacNamespaceEditor.php +++ b/src/applications/almanac/editor/AlmanacNamespaceEditor.php @@ -1,16 +1,20 @@ getTransactionType()) { - case AlmanacNamespaceTransaction::TYPE_NAME: - return $object->getName(); - } - - return parent::getCustomTransactionOldValue($object, $xaction); - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacNamespaceTransaction::TYPE_NAME: - return $xaction->getNewValue(); - } - - return parent::getCustomTransactionNewValue($object, $xaction); - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacNamespaceTransaction::TYPE_NAME: - $object->setName($xaction->getNewValue()); - return; - } - - return parent::applyCustomInternalTransaction($object, $xaction); - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacNamespaceTransaction::TYPE_NAME: - return; - } - - return parent::applyCustomExternalTransaction($object, $xaction); - } - - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - - switch ($type) { - case AlmanacNamespaceTransaction::TYPE_NAME: - $missing = $this->validateIsEmptyTextField( - $object->getName(), - $xactions); - - if ($missing) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht('Namespace name is required.'), - nonempty(last($xactions), null)); - - $error->setIsMissingFieldError(true); - $errors[] = $error; - } else { - foreach ($xactions as $xaction) { - $name = $xaction->getNewValue(); - - $message = null; - try { - AlmanacNames::validateName($name); - } catch (Exception $ex) { - $message = $ex->getMessage(); - } - - if ($message !== null) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - $message, - $xaction); - $errors[] = $error; - continue; - } - - $other = id(new AlmanacNamespaceQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withNames(array($name)) - ->executeOne(); - if ($other && ($other->getID() != $object->getID())) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Not Unique'), - pht( - 'The namespace name "%s" is already in use by another '. - 'namespace. Each namespace must have a unique name.', - $name), - $xaction); - $errors[] = $error; - continue; - } - - if ($name === $object->getName()) { - continue; - } - - $namespace = AlmanacNamespace::loadRestrictedNamespace( - $this->getActor(), - $name); - if ($namespace) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Restricted'), - pht( - 'You do not have permission to create Almanac namespaces '. - 'within the "%s" namespace.', - $namespace->getName()), - $xaction); - $errors[] = $error; - continue; - } - } - } - - break; - } - - return $errors; - } - protected function didCatchDuplicateKeyException( PhabricatorLiskDAO $object, array $xactions, diff --git a/src/applications/almanac/editor/AlmanacNetworkEditEngine.php b/src/applications/almanac/editor/AlmanacNetworkEditEngine.php index e474e81291..027b2a9aa0 100644 --- a/src/applications/almanac/editor/AlmanacNetworkEditEngine.php +++ b/src/applications/almanac/editor/AlmanacNetworkEditEngine.php @@ -81,7 +81,7 @@ final class AlmanacNetworkEditEngine ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the network.')) - ->setTransactionType(AlmanacNetworkTransaction::TYPE_NAME) + ->setTransactionType(AlmanacNetworkNameTransaction::TRANSACTIONTYPE) ->setIsRequired(true) ->setValue($object->getName()), ); diff --git a/src/applications/almanac/editor/AlmanacNetworkEditor.php b/src/applications/almanac/editor/AlmanacNetworkEditor.php index f32b58219e..0ea5778621 100644 --- a/src/applications/almanac/editor/AlmanacNetworkEditor.php +++ b/src/applications/almanac/editor/AlmanacNetworkEditor.php @@ -1,16 +1,20 @@ getTransactionType()) { - case AlmanacNetworkTransaction::TYPE_NAME: - return $object->getName(); - } - - return parent::getCustomTransactionOldValue($object, $xaction); - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacNetworkTransaction::TYPE_NAME: - return $xaction->getNewValue(); - } - - return parent::getCustomTransactionNewValue($object, $xaction); - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacNetworkTransaction::TYPE_NAME: - $object->setName($xaction->getNewValue()); - return; - } - - return parent::applyCustomInternalTransaction($object, $xaction); - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacNetworkTransaction::TYPE_NAME: - return; - } - - return parent::applyCustomExternalTransaction($object, $xaction); - } - - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - - switch ($type) { - case AlmanacNetworkTransaction::TYPE_NAME: - $missing = $this->validateIsEmptyTextField( - $object->getName(), - $xactions); - - if ($missing) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht('Network name is required.'), - nonempty(last($xactions), null)); - - $error->setIsMissingFieldError(true); - $errors[] = $error; - } - break; - } - - return $errors; - } - - - } diff --git a/src/applications/almanac/editor/AlmanacPropertyEditEngine.php b/src/applications/almanac/editor/AlmanacPropertyEditEngine.php index c22a3ab869..38139f79fa 100644 --- a/src/applications/almanac/editor/AlmanacPropertyEditEngine.php +++ b/src/applications/almanac/editor/AlmanacPropertyEditEngine.php @@ -64,10 +64,17 @@ abstract class AlmanacPropertyEditEngine protected function buildCustomEditFields($object) { $property_key = $this->getPropertyKey(); - $xaction_type = AlmanacTransaction::TYPE_PROPERTY_UPDATE; + $xaction_type = $object->getAlmanacPropertySetTransactionType(); + + $specs = $object->getAlmanacPropertyFieldSpecifications(); + if (isset($specs[$property_key])) { + $field_template = clone $specs[$property_key]; + } else { + $field_template = new PhabricatorTextEditField(); + } return array( - id(new PhabricatorTextEditField()) + $field_template ->setKey('value') ->setMetadataValue('almanac.property', $property_key) ->setLabel($property_key) diff --git a/src/applications/almanac/editor/AlmanacServiceEditEngine.php b/src/applications/almanac/editor/AlmanacServiceEditEngine.php index e64cfce3a2..00e54962b3 100644 --- a/src/applications/almanac/editor/AlmanacServiceEditEngine.php +++ b/src/applications/almanac/editor/AlmanacServiceEditEngine.php @@ -41,8 +41,46 @@ final class AlmanacServiceEditEngine return AlmanacService::initializeNewService($service_type); } + protected function newEditableObjectFromConduit(array $raw_xactions) { + $type = null; + foreach ($raw_xactions as $raw_xaction) { + if ($raw_xaction['type'] !== 'type') { + continue; + } + + $type = $raw_xaction['value']; + } + + if ($type === null) { + throw new Exception( + pht( + 'When creating a new Almanac service via the Conduit API, you '. + 'must provide a "type" transaction to select a type.')); + } + + $map = AlmanacServiceType::getAllServiceTypes(); + if (!isset($map[$type])) { + throw new Exception( + pht( + 'Service type "%s" is unrecognized. Valid types are: %s.', + $type, + implode(', ', array_keys($map)))); + } + + $this->setServiceType($type); + + return $this->newEditableObject(); + } + + protected function newEditableObjectForDocumentation() { + $service_type = new AlmanacCustomServiceType(); + $this->setServiceType($service_type->getServiceTypeConstant()); + return $this->newEditableObject(); + } + protected function newObjectQuery() { - return new AlmanacServiceQuery(); + return id(new AlmanacServiceQuery()) + ->needProperties(true); } protected function getObjectCreateTitleText($object) { @@ -92,9 +130,19 @@ final class AlmanacServiceEditEngine ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the service.')) - ->setTransactionType(AlmanacServiceTransaction::TYPE_NAME) + ->setTransactionType(AlmanacServiceNameTransaction::TRANSACTIONTYPE) ->setIsRequired(true) ->setValue($object->getName()), + id(new PhabricatorTextEditField()) + ->setKey('type') + ->setLabel(pht('Type')) + ->setIsConduitOnly(true) + ->setTransactionType( + AlmanacServiceTypeTransaction::TRANSACTIONTYPE) + ->setDescription(pht('When creating a service, set the type.')) + ->setConduitDescription(pht('Set the service type.')) + ->setConduitTypeDescription(pht('Service type.')) + ->setValue($object->getServiceType()), ); } diff --git a/src/applications/almanac/editor/AlmanacServiceEditor.php b/src/applications/almanac/editor/AlmanacServiceEditor.php index 8b0326f749..ba509dae34 100644 --- a/src/applications/almanac/editor/AlmanacServiceEditor.php +++ b/src/applications/almanac/editor/AlmanacServiceEditor.php @@ -7,152 +7,23 @@ final class AlmanacServiceEditor return pht('Almanac Service'); } + public function getCreateObjectTitle($author, $object) { + return pht('%s created this service.', $author); + } + + public function getCreateObjectTitleForFeed($author, $object) { + return pht('%s created %s.', $author, $object); + } + public function getTransactionTypes() { $types = parent::getTransactionTypes(); - $types[] = AlmanacServiceTransaction::TYPE_NAME; - $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; return $types; } - protected function getCustomTransactionOldValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { - case AlmanacServiceTransaction::TYPE_NAME: - return $object->getName(); - } - - return parent::getCustomTransactionOldValue($object, $xaction); - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacServiceTransaction::TYPE_NAME: - return $xaction->getNewValue(); - } - - return parent::getCustomTransactionNewValue($object, $xaction); - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacServiceTransaction::TYPE_NAME: - $object->setName($xaction->getNewValue()); - return; - } - - return parent::applyCustomInternalTransaction($object, $xaction); - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case AlmanacServiceTransaction::TYPE_NAME: - return; - } - - return parent::applyCustomExternalTransaction($object, $xaction); - } - - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - - switch ($type) { - case AlmanacServiceTransaction::TYPE_NAME: - $missing = $this->validateIsEmptyTextField( - $object->getName(), - $xactions); - - if ($missing) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht('Service name is required.'), - nonempty(last($xactions), null)); - - $error->setIsMissingFieldError(true); - $errors[] = $error; - } else { - foreach ($xactions as $xaction) { - $message = null; - - $name = $xaction->getNewValue(); - - try { - AlmanacNames::validateName($name); - } catch (Exception $ex) { - $message = $ex->getMessage(); - } - - if ($message !== null) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - $message, - $xaction); - $errors[] = $error; - continue; - } - - $other = id(new AlmanacServiceQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withNames(array($name)) - ->executeOne(); - if ($other && ($other->getID() != $object->getID())) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Not Unique'), - pht('Almanac services must have unique names.'), - last($xactions)); - $errors[] = $error; - continue; - } - - if ($name === $object->getName()) { - continue; - } - - $namespace = AlmanacNamespace::loadRestrictedNamespace( - $this->getActor(), - $name); - if ($namespace) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Restricted'), - pht( - 'You do not have permission to create Almanac services '. - 'within the "%s" namespace.', - $namespace->getName()), - $xaction); - $errors[] = $error; - continue; - } - } - } - - break; - } - - return $errors; - } - - protected function validateAllTransactions( PhabricatorLiskDAO $object, array $xactions) { diff --git a/src/applications/almanac/engineextension/AlmanacDeletePropertyEditField.php b/src/applications/almanac/engineextension/AlmanacDeletePropertyEditField.php new file mode 100644 index 0000000000..82aa4b82b5 --- /dev/null +++ b/src/applications/almanac/engineextension/AlmanacDeletePropertyEditField.php @@ -0,0 +1,22 @@ + $property_key) { + if (!is_string($property_key)) { + throw new Exception( + pht( + 'When deleting Almanac properties, each property name must '. + 'be a string. The value at index "%s" is not a string.', + $idx)); + } + + $xactions[] = $this->newTransaction($template) + ->setMetadataValue('almanac.property', $property_key) + ->setNewValue(true); + } + + return $xactions; + } + +} diff --git a/src/applications/almanac/engineextension/AlmanacPropertiesEditEngineExtension.php b/src/applications/almanac/engineextension/AlmanacPropertiesEditEngineExtension.php new file mode 100644 index 0000000000..965c193f40 --- /dev/null +++ b/src/applications/almanac/engineextension/AlmanacPropertiesEditEngineExtension.php @@ -0,0 +1,44 @@ +setKey('property.set') + ->setTransactionType($object->getAlmanacPropertySetTransactionType()) + ->setConduitDescription( + pht('Pass a map of values to set one or more properties.')) + ->setConduitTypeDescription(pht('Map of property names to values.')) + ->setIsConduitOnly(true), + id(new AlmanacDeletePropertyEditField()) + ->setKey('property.delete') + ->setTransactionType($object->getAlmanacPropertyDeleteTransactionType()) + ->setConduitDescription( + pht('Pass a list of property names to delete properties.')) + ->setConduitTypeDescription(pht('List of property names.')) + ->setIsConduitOnly(true), + ); + } + +} diff --git a/src/applications/almanac/engineextension/AlmanacSetPropertyEditField.php b/src/applications/almanac/engineextension/AlmanacSetPropertyEditField.php new file mode 100644 index 0000000000..aec3ea6c9f --- /dev/null +++ b/src/applications/almanac/engineextension/AlmanacSetPropertyEditField.php @@ -0,0 +1,22 @@ + $property_value) { + $xactions[] = $this->newTransaction($template) + ->setMetadataValue('almanac.property', $property_key) + ->setNewValue($property_value); + } + + return $xactions; + } + +} diff --git a/src/applications/almanac/management/AlmanacManagementWorkflow.php b/src/applications/almanac/management/AlmanacManagementWorkflow.php index 0f1dd6d773..5d466293c4 100644 --- a/src/applications/almanac/management/AlmanacManagementWorkflow.php +++ b/src/applications/almanac/management/AlmanacManagementWorkflow.php @@ -26,20 +26,4 @@ abstract class AlmanacManagementWorkflow return $services; } - protected function updateServiceLock(AlmanacService $service, $lock) { - $almanac_phid = id(new PhabricatorAlmanacApplication())->getPHID(); - - $xaction = id(new AlmanacServiceTransaction()) - ->setTransactionType(AlmanacServiceTransaction::TYPE_LOCK) - ->setNewValue((int)$lock); - - $editor = id(new AlmanacServiceEditor()) - ->setActor($this->getViewer()) - ->setActingAsPHID($almanac_phid) - ->setContentSource($this->newContentSource()) - ->setContinueOnMissingFields(true); - - $editor->applyTransactions($service, array($xaction)); - } - } diff --git a/src/applications/almanac/property/AlmanacPropertyInterface.php b/src/applications/almanac/property/AlmanacPropertyInterface.php index b50b4089a8..6f3224ddee 100644 --- a/src/applications/almanac/property/AlmanacPropertyInterface.php +++ b/src/applications/almanac/property/AlmanacPropertyInterface.php @@ -9,5 +9,7 @@ interface AlmanacPropertyInterface { public function getAlmanacPropertyValue($key, $default = null); public function getAlmanacPropertyFieldSpecifications(); public function newAlmanacPropertyEditEngine(); + public function getAlmanacPropertySetTransactionType(); + public function getAlmanacPropertyDeleteTransactionType(); } diff --git a/src/applications/almanac/query/AlmanacBindingSearchEngine.php b/src/applications/almanac/query/AlmanacBindingSearchEngine.php new file mode 100644 index 0000000000..fbeb7644b9 --- /dev/null +++ b/src/applications/almanac/query/AlmanacBindingSearchEngine.php @@ -0,0 +1,80 @@ +setLabel(pht('Services')) + ->setKey('servicePHIDs') + ->setAliases(array('service', 'servicePHID', 'services')) + ->setDescription(pht('Search for bindings on particular services.')), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Devices')) + ->setKey('devicePHIDs') + ->setAliases(array('device', 'devicePHID', 'devices')) + ->setDescription(pht('Search for bindings on particular devices.')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['servicePHIDs']) { + $query->withServicePHIDs($map['servicePHIDs']); + } + + if ($map['devicePHIDs']) { + $query->withDevicePHIDs($map['devicePHIDs']); + } + + return $query; + } + + protected function getURI($path) { + return '/almanac/binding/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array( + 'all' => pht('All Bindings'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $devices, + PhabricatorSavedQuery $query, + array $handles) { + + // For now, this SearchEngine just supports API access via Conduit. + throw new PhutilMethodNotImplementedException(); + } + +} diff --git a/src/applications/almanac/query/AlmanacInterfaceSearchEngine.php b/src/applications/almanac/query/AlmanacInterfaceSearchEngine.php new file mode 100644 index 0000000000..9a3c5bc8f0 --- /dev/null +++ b/src/applications/almanac/query/AlmanacInterfaceSearchEngine.php @@ -0,0 +1,71 @@ +setLabel(pht('Devices')) + ->setKey('devicePHIDs') + ->setAliases(array('device', 'devicePHID', 'devices')) + ->setDescription(pht('Search for interfaces on particular devices.')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['devicePHIDs']) { + $query->withDevicePHIDs($map['devicePHIDs']); + } + + return $query; + } + + protected function getURI($path) { + return '/almanac/interface/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array( + 'all' => pht('All Interfaces'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $devices, + PhabricatorSavedQuery $query, + array $handles) { + + // For now, this SearchEngine just supports API access via Conduit. + throw new PhutilMethodNotImplementedException(); + } + +} diff --git a/src/applications/almanac/query/AlmanacQuery.php b/src/applications/almanac/query/AlmanacQuery.php index b046171d32..b5b6146c7f 100644 --- a/src/applications/almanac/query/AlmanacQuery.php +++ b/src/applications/almanac/query/AlmanacQuery.php @@ -34,10 +34,12 @@ abstract class AlmanacQuery $specs = $object->getAlmanacPropertyFieldSpecifications(); foreach ($specs as $key => $spec) { if (empty($object_properties[$key])) { + $default_value = $spec->getValueForTransaction(); + $object_properties[$key] = id(new AlmanacProperty()) ->setObjectPHID($object->getPHID()) ->setFieldName($key) - ->setFieldValue($spec->getValueForTransaction()); + ->setFieldValue($default_value); } } diff --git a/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php b/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php index 259092b5b1..98947f56c1 100644 --- a/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php @@ -20,7 +20,48 @@ final class AlmanacClusterRepositoryServiceType public function getFieldSpecifications() { return array( - 'closed' => id(new PhabricatorTextEditField()), + 'closed' => id(new PhabricatorBoolEditField()) + ->setOptions( + pht('Allow New Repositories'), + pht('Prevent New Repositories')) + ->setValue(false), + ); + } + + public function getBindingFieldSpecifications(AlmanacBinding $binding) { + $protocols = array( + array( + 'value' => 'http', + 'port' => 80, + ), + array( + 'value' => 'https', + 'port' => 443, + ), + array( + 'value' => 'ssh', + 'port' => 22, + ), + ); + + $default_value = 'http'; + if ($binding->hasInterface()) { + $interface = $binding->getInterface(); + $port = $interface->getPort(); + + $default_ports = ipull($protocols, 'value', 'port'); + $default_value = idx($default_ports, $port, $default_value); + } + + return array( + 'protocol' => id(new PhabricatorSelectEditField()) + ->setOptions(ipull($protocols, 'value', 'value')) + ->setValue($default_value), + 'writable' => id(new PhabricatorBoolEditField()) + ->setOptions( + pht('Prevent Writes'), + pht('Allow Writes')) + ->setValue(true), ); } diff --git a/src/applications/almanac/servicetype/AlmanacServiceType.php b/src/applications/almanac/servicetype/AlmanacServiceType.php index 2eb56dc8c4..2f7f503fe8 100644 --- a/src/applications/almanac/servicetype/AlmanacServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacServiceType.php @@ -60,6 +60,10 @@ abstract class AlmanacServiceType extends Phobject { return array(); } + public function getBindingFieldSpecifications(AlmanacBinding $binding) { + return array(); + } + /** * List all available service type implementations. * diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php index 9d0fbc1622..c593e40fa7 100644 --- a/src/applications/almanac/storage/AlmanacBinding.php +++ b/src/applications/almanac/storage/AlmanacBinding.php @@ -7,7 +7,8 @@ final class AlmanacBinding PhabricatorApplicationTransactionInterface, AlmanacPropertyInterface, PhabricatorDestructibleInterface, - PhabricatorExtendedPolicyInterface { + PhabricatorExtendedPolicyInterface, + PhabricatorConduitResultInterface { protected $servicePHID; protected $devicePHID; @@ -23,6 +24,7 @@ final class AlmanacBinding public static function initializeNewBinding(AlmanacService $service) { return id(new AlmanacBinding()) ->setServicePHID($service->getPHID()) + ->attachService($service) ->attachAlmanacProperties(array()) ->setIsDisabled(0); } @@ -86,6 +88,10 @@ final class AlmanacBinding return $this; } + public function hasInterface() { + return ($this->interface !== self::ATTACHABLE); + } + public function getInterface() { return $this->assertAttached($this->interface); } @@ -127,13 +133,21 @@ final class AlmanacBinding } public function getAlmanacPropertyFieldSpecifications() { - return array(); + return $this->getService()->getBindingFieldSpecifications($this); } public function newAlmanacPropertyEditEngine() { return new AlmanacBindingPropertyEditEngine(); } + public function getAlmanacPropertySetTransactionType() { + return AlmanacBindingSetPropertyTransaction::TRANSACTIONTYPE; + } + + public function getAlmanacPropertyDeleteTransactionType() { + return AlmanacBindingDeletePropertyTransaction::TRANSACTIONTYPE; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -217,4 +231,44 @@ final class AlmanacBinding } +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('servicePHID') + ->setType('phid') + ->setDescription(pht('The bound service.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('devicePHID') + ->setType('phid') + ->setDescription(pht('The device the service is bound to.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('interfacePHID') + ->setType('phid') + ->setDescription(pht('The interface the service is bound to.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('disabled') + ->setType('bool') + ->setDescription(pht('Interface status.')), + ); + } + + public function getFieldValuesForConduit() { + return array( + 'servicePHID' => $this->getServicePHID(), + 'devicePHID' => $this->getDevicePHID(), + 'interfacePHID' => $this->getInterfacePHID(), + 'disabled' => (bool)$this->getIsDisabled(), + ); + } + + public function getConduitSearchAttachments() { + return array( + id(new AlmanacPropertiesSearchEngineAttachment()) + ->setAttachmentKey('properties'), + ); + } + } diff --git a/src/applications/almanac/storage/AlmanacBindingTransaction.php b/src/applications/almanac/storage/AlmanacBindingTransaction.php index 11120f9827..d17eb59000 100644 --- a/src/applications/almanac/storage/AlmanacBindingTransaction.php +++ b/src/applications/almanac/storage/AlmanacBindingTransaction.php @@ -1,77 +1,14 @@ getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_INTERFACE: - if ($old) { - $phids[] = $old; - } - if ($new) { - $phids[] = $new; - } - break; - } - - return $phids; - } - - public function getTitle() { - $author_phid = $this->getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_INTERFACE: - if ($old === null) { - return pht( - '%s created this binding.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s changed this binding from %s to %s.', - $this->renderHandleLink($author_phid), - $this->renderHandleLink($old), - $this->renderHandleLink($new)); - } - break; - case self::TYPE_DISABLE: - if ($new) { - return pht( - '%s disabled this binding.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s enabled this binding.', - $this->renderHandleLink($author_phid)); - } - break; - } - - return parent::getTitle(); + public function getBaseTransactionClass() { + return 'AlmanacBindingTransactionType'; } } diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index 2f89e6c2b6..a1ebdfffae 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -143,6 +143,14 @@ final class AlmanacDevice return new AlmanacDevicePropertyEditEngine(); } + public function getAlmanacPropertySetTransactionType() { + return AlmanacDeviceSetPropertyTransaction::TRANSACTIONTYPE; + } + + public function getAlmanacPropertyDeleteTransactionType() { + return AlmanacDeviceDeletePropertyTransaction::TRANSACTIONTYPE; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacDeviceTransaction.php b/src/applications/almanac/storage/AlmanacDeviceTransaction.php index fd1a21f782..2b921525ac 100644 --- a/src/applications/almanac/storage/AlmanacDeviceTransaction.php +++ b/src/applications/almanac/storage/AlmanacDeviceTransaction.php @@ -1,100 +1,14 @@ getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_INTERFACE: - if ($old) { - $phids[] = $old['networkPHID']; - } - if ($new) { - $phids[] = $new['networkPHID']; - } - break; - } - - return $phids; - } - - public function getTitle() { - $author_phid = $this->getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_NAME: - if ($old === null) { - return pht( - '%s created this device.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s renamed this device from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - break; - case self::TYPE_INTERFACE: - if ($old && $new) { - return pht( - '%s changed interface %s on this device to %s.', - $this->renderHandleLink($author_phid), - $this->describeInterface($old), - $this->describeInterface($new)); - } else if ($old) { - return pht( - '%s removed the interface %s from this device.', - $this->renderHandleLink($author_phid), - $this->describeInterface($old)); - } else if ($new) { - return pht( - '%s added the interface %s to this device.', - $this->renderHandleLink($author_phid), - $this->describeInterface($new)); - } - } - - return parent::getTitle(); - } - - public function shouldGenerateOldValue() { - switch ($this->getTransactionType()) { - case self::TYPE_INTERFACE: - return false; - } - return parent::shouldGenerateOldValue(); - } - - private function describeInterface(array $info) { - return pht( - '%s:%s (%s)', - $info['address'], - $info['port'], - $this->renderHandleLink($info['networkPHID'])); + public function getBaseTransactionClass() { + return 'AlmanacDeviceTransactionType'; } } diff --git a/src/applications/almanac/storage/AlmanacInterface.php b/src/applications/almanac/storage/AlmanacInterface.php index d55c88b791..4002651d08 100644 --- a/src/applications/almanac/storage/AlmanacInterface.php +++ b/src/applications/almanac/storage/AlmanacInterface.php @@ -5,7 +5,9 @@ final class AlmanacInterface implements PhabricatorPolicyInterface, PhabricatorDestructibleInterface, - PhabricatorExtendedPolicyInterface { + PhabricatorExtendedPolicyInterface, + PhabricatorApplicationTransactionInterface, + PhabricatorConduitResultInterface { protected $devicePHID; protected $networkPHID; @@ -154,4 +156,64 @@ final class AlmanacInterface $this->delete(); } + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new AlmanacInterfaceEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new AlmanacInterfaceTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + return $timeline; + } + + +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('devicePHID') + ->setType('phid') + ->setDescription(pht('The device the interface is on.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('networkPHID') + ->setType('phid') + ->setDescription(pht('The network the interface is part of.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('address') + ->setType('string') + ->setDescription(pht('The address of the interface.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('port') + ->setType('int') + ->setDescription(pht('The port number of the interface.')), + ); + } + + public function getFieldValuesForConduit() { + return array( + 'devicePHID' => $this->getDevicePHID(), + 'networkPHID' => $this->getNetworkPHID(), + 'address' => (string)$this->getAddress(), + 'port' => (int)$this->getPort(), + ); + } + + public function getConduitSearchAttachments() { + return array(); + } + } diff --git a/src/applications/almanac/storage/AlmanacInterfaceTransaction.php b/src/applications/almanac/storage/AlmanacInterfaceTransaction.php new file mode 100644 index 0000000000..02263f13c2 --- /dev/null +++ b/src/applications/almanac/storage/AlmanacInterfaceTransaction.php @@ -0,0 +1,14 @@ +setKey('name') + ->setType('string') + ->setDescription(pht('The name of the namespace.')), + ); + } + + public function getFieldValuesForConduit() { + return array( + 'name' => $this->getName(), + ); + } + + public function getConduitSearchAttachments() { + return array(); + } + + } diff --git a/src/applications/almanac/storage/AlmanacNamespaceTransaction.php b/src/applications/almanac/storage/AlmanacNamespaceTransaction.php index 9461fa5f13..7b859832b8 100644 --- a/src/applications/almanac/storage/AlmanacNamespaceTransaction.php +++ b/src/applications/almanac/storage/AlmanacNamespaceTransaction.php @@ -1,43 +1,14 @@ getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case PhabricatorTransactions::TYPE_CREATE: - return pht( - '%s created this namespace.', - $this->renderHandleLink($author_phid)); - break; - case self::TYPE_NAME: - return pht( - '%s renamed this namespace from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - - return parent::getTitle(); + public function getBaseTransactionClass() { + return 'AlmanacNamespaceTransactionType'; } } diff --git a/src/applications/almanac/storage/AlmanacNetwork.php b/src/applications/almanac/storage/AlmanacNetwork.php index 737f38faec..6a530b4cb3 100644 --- a/src/applications/almanac/storage/AlmanacNetwork.php +++ b/src/applications/almanac/storage/AlmanacNetwork.php @@ -6,7 +6,8 @@ final class AlmanacNetwork PhabricatorApplicationTransactionInterface, PhabricatorPolicyInterface, PhabricatorDestructibleInterface, - PhabricatorNgramsInterface { + PhabricatorNgramsInterface, + PhabricatorConduitResultInterface { protected $name; protected $mailKey; @@ -122,4 +123,27 @@ final class AlmanacNetwork ); } + +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('name') + ->setType('string') + ->setDescription(pht('The name of the network.')), + ); + } + + public function getFieldValuesForConduit() { + return array( + 'name' => $this->getName(), + ); + } + + public function getConduitSearchAttachments() { + return array(); + } + } diff --git a/src/applications/almanac/storage/AlmanacNetworkTransaction.php b/src/applications/almanac/storage/AlmanacNetworkTransaction.php index f8c2b43b0b..e487ffda3d 100644 --- a/src/applications/almanac/storage/AlmanacNetworkTransaction.php +++ b/src/applications/almanac/storage/AlmanacNetworkTransaction.php @@ -1,42 +1,14 @@ getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case PhabricatorTransactions::TYPE_CREATE: - return pht( - '%s created this network.', - $this->renderHandleLink($author_phid)); - case self::TYPE_NAME: - return pht( - '%s renamed this network from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - - return parent::getTitle(); + public function getBaseTransactionClass() { + return 'AlmanacNetworkTransactionType'; } } diff --git a/src/applications/almanac/storage/AlmanacProperty.php b/src/applications/almanac/storage/AlmanacProperty.php index 0d317f8c8e..bd3fe83347 100644 --- a/src/applications/almanac/storage/AlmanacProperty.php +++ b/src/applications/almanac/storage/AlmanacProperty.php @@ -54,7 +54,7 @@ final class AlmanacProperty } $xactions[] = id(clone $template) - ->setTransactionType(AlmanacTransaction::TYPE_PROPERTY_UPDATE) + ->setTransactionType($object->getAlmanacPropertySetTransactionType()) ->setMetadataValue('almanac.property', $name) ->setNewValue($property); } @@ -71,7 +71,7 @@ final class AlmanacProperty $xactions = array(); foreach ($properties as $property) { $xactions[] = id(clone $template) - ->setTransactionType(AlmanacTransaction::TYPE_PROPERTY_REMOVE) + ->setTransactionType($object->getAlmanacPropertyDeleteTransactionType()) ->setMetadataValue('almanac.property', $property) ->setNewValue(null); } diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index 9ef42d6407..ee40d83400 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -156,10 +156,23 @@ final class AlmanacService return $this->getServiceImplementation()->getFieldSpecifications(); } + public function getBindingFieldSpecifications(AlmanacBinding $binding) { + $impl = $this->getServiceImplementation(); + return $impl->getBindingFieldSpecifications($binding); + } + public function newAlmanacPropertyEditEngine() { return new AlmanacServicePropertyEditEngine(); } + public function getAlmanacPropertySetTransactionType() { + return AlmanacServiceSetPropertyTransaction::TRANSACTIONTYPE; + } + + public function getAlmanacPropertyDeleteTransactionType() { + return AlmanacServiceDeletePropertyTransaction::TRANSACTIONTYPE; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacServiceTransaction.php b/src/applications/almanac/storage/AlmanacServiceTransaction.php index a668f860fa..62b4137e7b 100644 --- a/src/applications/almanac/storage/AlmanacServiceTransaction.php +++ b/src/applications/almanac/storage/AlmanacServiceTransaction.php @@ -1,49 +1,14 @@ getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_NAME: - if ($old === null) { - return pht( - '%s created this service.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s renamed this service from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - break; - case self::TYPE_LOCK: - if ($new) { - return pht( - '%s locked this service.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s unlocked this service.', - $this->renderHandleLink($author_phid)); - } - break; - } - - return parent::getTitle(); + public function getBaseTransactionClass() { + return 'AlmanacServiceTransactionType'; } } diff --git a/src/applications/almanac/storage/AlmanacTransaction.php b/src/applications/almanac/storage/AlmanacTransaction.php deleted file mode 100644 index 18bd776335..0000000000 --- a/src/applications/almanac/storage/AlmanacTransaction.php +++ /dev/null @@ -1,38 +0,0 @@ -getAuthorPHID(); - - switch ($this->getTransactionType()) { - case self::TYPE_PROPERTY_UPDATE: - $property_key = $this->getMetadataValue('almanac.property'); - return pht( - '%s updated the property "%s".', - $this->renderHandleLink($author_phid), - $property_key); - case self::TYPE_PROPERTY_REMOVE: - $property_key = $this->getMetadataValue('almanac.property'); - return pht( - '%s deleted the property "%s".', - $this->renderHandleLink($author_phid), - $property_key); - } - - return parent::getTitle(); - } - -} diff --git a/src/applications/almanac/xaction/AlmanacBindingDeletePropertyTransaction.php b/src/applications/almanac/xaction/AlmanacBindingDeletePropertyTransaction.php new file mode 100644 index 0000000000..aece12c9bf --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacBindingDeletePropertyTransaction.php @@ -0,0 +1,20 @@ +getAlmanacPropertyOldValue($object); + } + + public function applyInternalEffects($object, $value) { + return $this->deleteAlmanacProperty($object); + } + + public function getTitle() { + return $this->getAlmanacDeletePropertyTitle(); + } + +} diff --git a/src/applications/almanac/xaction/AlmanacBindingDisableTransaction.php b/src/applications/almanac/xaction/AlmanacBindingDisableTransaction.php new file mode 100644 index 0000000000..1592686835 --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacBindingDisableTransaction.php @@ -0,0 +1,28 @@ +getIsDisabled(); + } + + public function applyInternalEffects($object, $value) { + $object->setIsDisabled((int)$value); + } + + public function getTitle() { + if ($this->getNewValue()) { + return pht( + '%s disabled this binding.', + $this->renderAuthor()); + } else { + return pht( + '%s enabled this binding.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php b/src/applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php new file mode 100644 index 0000000000..f43fcdfa86 --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php @@ -0,0 +1,111 @@ +getInterfacePHID(); + } + + public function applyInternalEffects($object, $value) { + $interface = $this->loadInterface($value); + + $object + ->setDevicePHID($interface->getDevicePHID()) + ->setInterfacePHID($interface->getPHID()); + } + + public function applyExternalEffects($object, $value) { + + // When we change which services a device is bound to, we need to + // recalculate whether it is a cluster device or not so we can tell if + // the "Can Manage Cluster Services" permission applies to it. + + $viewer = PhabricatorUser::getOmnipotentUser(); + $interface_phids = array(); + + $interface_phids[] = $this->getOldValue(); + $interface_phids[] = $this->getNewValue(); + + $interface_phids = array_filter($interface_phids); + $interface_phids = array_unique($interface_phids); + + $interfaces = id(new AlmanacInterfaceQuery()) + ->setViewer($viewer) + ->withPHIDs($interface_phids) + ->execute(); + + $device_phids = array(); + foreach ($interfaces as $interface) { + $device_phids[] = $interface->getDevicePHID(); + } + + $device_phids = array_unique($device_phids); + + $devices = id(new AlmanacDeviceQuery()) + ->setViewer($viewer) + ->withPHIDs($device_phids) + ->execute(); + + foreach ($devices as $device) { + $device->rebuildClusterBindingStatus(); + } + } + + public function getTitle() { + return pht( + '%s changed the interface for this binding from %s to %s.', + $this->renderAuthor(), + $this->renderOldHandle(), + $this->renderNewHandle()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $interface_phid = $object->getInterfacePHID(); + if ($this->isEmptyTextTransaction($interface_phid, $xactions)) { + $errors[] = $this->newRequiredError( + pht('Bindings must specify an interface.')); + } + + foreach ($xactions as $xaction) { + $interface_phid = $xaction->getNewValue(); + + $interface = $this->loadInterface($interface_phid); + if (!$interface) { + $errors[] = $this->newInvalidError( + pht( + 'You can not bind a service to an invalid or restricted '. + 'interface.'), + $xaction); + continue; + } + + $binding = id(new AlmanacBindingQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withServicePHIDs(array($object->getServicePHID())) + ->withInterfacePHIDs(array($interface_phid)) + ->executeOne(); + if ($binding && ($binding->getID() != $object->getID())) { + $errors[] = $this->newInvalidError( + pht( + 'You can not bind a service to the same interface multiple '. + 'times.'), + $xaction); + continue; + } + } + + return $errors; + } + + private function loadInterface($phid) { + return id(new AlmanacInterfaceQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($phid)) + ->executeOne(); + } +} diff --git a/src/applications/almanac/xaction/AlmanacBindingServiceTransaction.php b/src/applications/almanac/xaction/AlmanacBindingServiceTransaction.php new file mode 100644 index 0000000000..4b9f802b27 --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacBindingServiceTransaction.php @@ -0,0 +1,64 @@ +getServicePHID(); + } + + public function applyInternalEffects($object, $value) { + $object->setServicePHID($value); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $service_phid = $object->getServicePHID(); + if ($this->isEmptyTextTransaction($service_phid, $xactions)) { + $errors[] = $this->newRequiredError( + pht('Bindings must have a service.')); + } + + foreach ($xactions as $xaction) { + if (!$this->isNewObject()) { + $errors[] = $this->newInvalidError( + pht( + 'The service for a binding can not be changed once it has '. + 'been created.'), + $xaction); + continue; + } + + $service_phid = $xaction->getNewValue(); + $services = id(new AlmanacServiceQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($service_phid)) + ->execute(); + if (!$services) { + $errors[] = $this->newInvalidError( + pht('You can not bind a nonexistent or restricted service.'), + $xaction); + continue; + } + + $service = head($services); + $can_edit = PhabricatorPolicyFilter::hasCapability( + $this->getActor(), + $service, + PhabricatorPolicyCapability::CAN_EDIT); + if (!$can_edit) { + $errors[] = $this->newInvalidError( + pht( + 'You can not bind a service which you do not have permission '. + 'to edit.')); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/almanac/xaction/AlmanacBindingSetPropertyTransaction.php b/src/applications/almanac/xaction/AlmanacBindingSetPropertyTransaction.php new file mode 100644 index 0000000000..cb1a7f9fa1 --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacBindingSetPropertyTransaction.php @@ -0,0 +1,24 @@ +getAlmanacPropertyOldValue($object); + } + + public function applyExternalEffects($object, $value) { + return $this->setAlmanacProperty($object, $value); + } + + public function getTitle() { + return $this->getAlmanacSetPropertyTitle(); + } + + public function validateTransactions($object, array $xactions) { + return $this->validateAlmanacSetPropertyTransactions($object, $xactions); + } + +} diff --git a/src/applications/almanac/xaction/AlmanacBindingTransactionType.php b/src/applications/almanac/xaction/AlmanacBindingTransactionType.php new file mode 100644 index 0000000000..3a3d9408b5 --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacBindingTransactionType.php @@ -0,0 +1,4 @@ +getAlmanacPropertyOldValue($object); + } + + public function applyInternalEffects($object, $value) { + return $this->deleteAlmanacProperty($object); + } + + public function getTitle() { + return $this->getAlmanacDeletePropertyTitle(); + } + +} diff --git a/src/applications/almanac/xaction/AlmanacDeviceNameTransaction.php b/src/applications/almanac/xaction/AlmanacDeviceNameTransaction.php new file mode 100644 index 0000000000..d72b380472 --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacDeviceNameTransaction.php @@ -0,0 +1,79 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + return pht( + '%s renamed this device from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Device name is required.')); + } + + foreach ($xactions as $xaction) { + $name = $xaction->getNewValue(); + + $message = null; + try { + AlmanacNames::validateName($name); + } catch (Exception $ex) { + $message = $ex->getMessage(); + } + + if ($message !== null) { + $errors[] = $this->newInvalidError($message, $xaction); + continue; + } + + if ($name === $object->getName()) { + continue; + } + + $other = id(new AlmanacDeviceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames(array($name)) + ->executeOne(); + if ($other && ($other->getID() != $object->getID())) { + $errors[] = $this->newInvalidError( + pht('Almanac devices must have unique names.'), + $xaction); + continue; + } + + $namespace = AlmanacNamespace::loadRestrictedNamespace( + $this->getActor(), + $name); + if ($namespace) { + $errors[] = $this->newInvalidError( + pht( + 'You do not have permission to create Almanac devices '. + 'within the "%s" namespace.', + $namespace->getName()), + $xaction); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/almanac/xaction/AlmanacDeviceSetPropertyTransaction.php b/src/applications/almanac/xaction/AlmanacDeviceSetPropertyTransaction.php new file mode 100644 index 0000000000..5170ec8ed9 --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacDeviceSetPropertyTransaction.php @@ -0,0 +1,24 @@ +getAlmanacPropertyOldValue($object); + } + + public function applyExternalEffects($object, $value) { + return $this->setAlmanacProperty($object, $value); + } + + public function getTitle() { + return $this->getAlmanacSetPropertyTitle(); + } + + public function validateTransactions($object, array $xactions) { + return $this->validateAlmanacSetPropertyTransactions($object, $xactions); + } + +} diff --git a/src/applications/almanac/xaction/AlmanacDeviceTransactionType.php b/src/applications/almanac/xaction/AlmanacDeviceTransactionType.php new file mode 100644 index 0000000000..cf3b239ab2 --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacDeviceTransactionType.php @@ -0,0 +1,4 @@ +getAddress(); + } + + public function applyInternalEffects($object, $value) { + $object->setAddress($value); + } + + public function getTitle() { + return pht( + '%s changed the address for this interface from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getAddress(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Interfaces must have an address.')); + } + + foreach ($xactions as $xaction) { + + // NOTE: For now, we don't validate addresses. We generally expect users + // to provide IPv4 addresses, but it's reasonable for them to provide + // IPv6 addresses, and some installs currently use DNS names. This is + // off-label but works today. + + } + + return $errors; + } + +} diff --git a/src/applications/almanac/xaction/AlmanacInterfaceDestroyTransaction.php b/src/applications/almanac/xaction/AlmanacInterfaceDestroyTransaction.php new file mode 100644 index 0000000000..5e829ff311 --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacInterfaceDestroyTransaction.php @@ -0,0 +1,32 @@ +destroyObject($object); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($xactions) { + if ($object->loadIsInUse()) { + $errors[] = $this->newInvalidError( + pht( + 'You can not delete this interface because it is currently in '. + 'use. One or more services are bound to it.')); + } + } + + return $errors; + } + +} diff --git a/src/applications/almanac/xaction/AlmanacInterfaceDeviceTransaction.php b/src/applications/almanac/xaction/AlmanacInterfaceDeviceTransaction.php new file mode 100644 index 0000000000..ed2c7b77de --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacInterfaceDeviceTransaction.php @@ -0,0 +1,74 @@ +getDevicePHID(); + } + + public function applyInternalEffects($object, $value) { + $object->setDevicePHID($value); + } + + public function getTitle() { + return pht( + '%s changed the device for this interface from %s to %s.', + $this->renderAuthor(), + $this->renderOldHandle(), + $this->renderNewHandle()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $device_phid = $object->getDevicePHID(); + if ($this->isEmptyTextTransaction($device_phid, $xactions)) { + $errors[] = $this->newRequiredError( + pht('Interfaces must have a device.')); + } + + foreach ($xactions as $xaction) { + if (!$this->isNewObject()) { + $errors[] = $this->newInvalidError( + pht( + 'The device for an interface can not be changed once it has '. + 'been created.'), + $xaction); + continue; + } + + $device_phid = $xaction->getNewValue(); + $devices = id(new AlmanacDeviceQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($device_phid)) + ->execute(); + if (!$devices) { + $errors[] = $this->newInvalidError( + pht( + 'You can not attach an interface to a nonexistent or restricted '. + 'device.'), + $xaction); + continue; + } + + $device = head($devices); + $can_edit = PhabricatorPolicyFilter::hasCapability( + $this->getActor(), + $device, + PhabricatorPolicyCapability::CAN_EDIT); + if (!$can_edit) { + $errors[] = $this->newInvalidError( + pht( + 'You can not attach an interface to a device which you do not '. + 'have permission to edit.')); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/almanac/xaction/AlmanacInterfaceNetworkTransaction.php b/src/applications/almanac/xaction/AlmanacInterfaceNetworkTransaction.php new file mode 100644 index 0000000000..def85c08e2 --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacInterfaceNetworkTransaction.php @@ -0,0 +1,53 @@ +getNetworkPHID(); + } + + public function applyInternalEffects($object, $value) { + $object->setNetworkPHID($value); + } + + public function getTitle() { + return pht( + '%s changed the network for this interface from %s to %s.', + $this->renderAuthor(), + $this->renderOldHandle(), + $this->renderNewHandle()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $network_phid = $object->getNetworkPHID(); + if ($this->isEmptyTextTransaction($network_phid, $xactions)) { + $errors[] = $this->newRequiredError( + pht('Interfaces must have a network.')); + } + + foreach ($xactions as $xaction) { + $network_phid = $xaction->getNewValue(); + + $networks = id(new AlmanacNetworkQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($network_phid)) + ->execute(); + if (!$networks) { + $errors[] = $this->newInvalidError( + pht( + 'You can not put an interface on a nonexistent or restricted '. + 'network.'), + $xaction); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/almanac/xaction/AlmanacInterfacePortTransaction.php b/src/applications/almanac/xaction/AlmanacInterfacePortTransaction.php new file mode 100644 index 0000000000..5e8e11b805 --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacInterfacePortTransaction.php @@ -0,0 +1,53 @@ +getPort(); + + if ($port !== null) { + $port = (int)$port; + } + + return $port; + } + + public function applyInternalEffects($object, $value) { + $object->setPort((int)$value); + } + + public function getTitle() { + return pht( + '%s changed the port for this interface from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getPort(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Interfaces must have a port number.')); + } + + foreach ($xactions as $xaction) { + $port = $xaction->getNewValue(); + + $port = (int)$port; + if ($port < 1 || $port > 65535) { + $errors[] = $this->newInvalidError( + pht('Port numbers must be between 1 and 65535, inclusive.'), + $xaction); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/almanac/xaction/AlmanacInterfaceTransactionType.php b/src/applications/almanac/xaction/AlmanacInterfaceTransactionType.php new file mode 100644 index 0000000000..cf13663a18 --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacInterfaceTransactionType.php @@ -0,0 +1,4 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + return pht( + '%s renamed this namespace from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function getTitleForFeed() { + return pht( + '%s renamed %s from %s to %s.', + $this->renderAuthor(), + $this->renderObject(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Namespace name is required.')); + } + + foreach ($xactions as $xaction) { + $name = $xaction->getNewValue(); + + $message = null; + try { + AlmanacNames::validateName($name); + } catch (Exception $ex) { + $message = $ex->getMessage(); + } + + if ($message !== null) { + $errors[] = $this->newInvalidError($message, $xaction); + continue; + } + + if ($name === $object->getName()) { + continue; + } + + $other = id(new AlmanacNamespaceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames(array($name)) + ->executeOne(); + if ($other && ($other->getID() != $object->getID())) { + $errors[] = $this->newInvalidError( + pht( + 'The namespace name "%s" is already in use by another '. + 'namespace. Each namespace must have a unique name.', + $name), + $xaction); + continue; + } + + $namespace = AlmanacNamespace::loadRestrictedNamespace( + $this->getActor(), + $name); + if ($namespace) { + $errors[] = $this->newInvalidError( + pht( + 'You do not have permission to create Almanac namespaces '. + 'within the "%s" namespace.', + $namespace->getName()), + $xaction); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/almanac/xaction/AlmanacNamespaceTransactionType.php b/src/applications/almanac/xaction/AlmanacNamespaceTransactionType.php new file mode 100644 index 0000000000..fee3d6d1eb --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacNamespaceTransactionType.php @@ -0,0 +1,4 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + return pht( + '%s renamed this network from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function getTitleForFeed() { + return pht( + '%s renamed %s from %s to %s.', + $this->renderAuthor(), + $this->renderObject(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Network name is required.')); + } + + return $errors; + } + +} diff --git a/src/applications/almanac/xaction/AlmanacNetworkTransactionType.php b/src/applications/almanac/xaction/AlmanacNetworkTransactionType.php new file mode 100644 index 0000000000..9f822ab09f --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacNetworkTransactionType.php @@ -0,0 +1,4 @@ +getAlmanacPropertyOldValue($object); + } + + public function applyInternalEffects($object, $value) { + return $this->deleteAlmanacProperty($object); + } + + public function getTitle() { + return $this->getAlmanacDeletePropertyTitle(); + } + +} diff --git a/src/applications/almanac/xaction/AlmanacServiceNameTransaction.php b/src/applications/almanac/xaction/AlmanacServiceNameTransaction.php new file mode 100644 index 0000000000..ea1ccb8faa --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacServiceNameTransaction.php @@ -0,0 +1,87 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + return pht( + '%s renamed this service from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function getTitleForFeed() { + return pht( + '%s renamed %s from %s to %s.', + $this->renderAuthor(), + $this->renderObject(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Almanac services must have a name.')); + } + + foreach ($xactions as $xaction) { + $name = $xaction->getNewValue(); + + $message = null; + try { + AlmanacNames::validateName($name); + } catch (Exception $ex) { + $message = $ex->getMessage(); + } + + if ($message !== null) { + $errors[] = $this->newInvalidError($message, $xaction); + continue; + } + + if ($name === $object->getName()) { + continue; + } + + $other = id(new AlmanacServiceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames(array($name)) + ->executeOne(); + if ($other && ($other->getID() != $object->getID())) { + $errors[] = $this->newInvalidError( + pht('Almanac services must have unique names.'), + $xaction); + continue; + } + + $namespace = AlmanacNamespace::loadRestrictedNamespace( + $this->getActor(), + $name); + if ($namespace) { + $errors[] = $this->newInvalidError( + pht( + 'You do not have permission to create Almanac services '. + 'within the "%s" namespace.', + $namespace->getName()), + $xaction); + continue; + } + } + + return $errors; + } +} diff --git a/src/applications/almanac/xaction/AlmanacServiceSetPropertyTransaction.php b/src/applications/almanac/xaction/AlmanacServiceSetPropertyTransaction.php new file mode 100644 index 0000000000..bd4863a986 --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacServiceSetPropertyTransaction.php @@ -0,0 +1,24 @@ +getAlmanacPropertyOldValue($object); + } + + public function applyExternalEffects($object, $value) { + return $this->setAlmanacProperty($object, $value); + } + + public function getTitle() { + return $this->getAlmanacSetPropertyTitle(); + } + + public function validateTransactions($object, array $xactions) { + return $this->validateAlmanacSetPropertyTransactions($object, $xactions); + } + +} diff --git a/src/applications/almanac/xaction/AlmanacServiceTransactionType.php b/src/applications/almanac/xaction/AlmanacServiceTransactionType.php new file mode 100644 index 0000000000..0e062806fd --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacServiceTransactionType.php @@ -0,0 +1,4 @@ +getServiceType(); + } + + public function applyInternalEffects($object, $value) { + $object->setServiceType($value); + } + + public function getTitle() { + // This transaction can only be applied during object creation via + // Conduit and never generates a timeline event. + return null; + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getServiceType(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('You must select a service type when creating a service.')); + } + + $map = AlmanacServiceType::getAllServiceTypes(); + + foreach ($xactions as $xaction) { + if (!$this->isNewObject()) { + $errors[] = $this->newInvalidError( + pht( + 'The type of a service can not be changed once it has '. + 'been created.'), + $xaction); + continue; + } + + $new = $xaction->getNewValue(); + if (!isset($map[$new])) { + $errors[] = $this->newInvalidError( + pht( + 'Service type "%s" is not valid. Valid types are: %s.', + $new, + implode(', ', array_keys($map)))); + continue; + } + } + + return $errors; + } +} diff --git a/src/applications/almanac/xaction/AlmanacTransactionType.php b/src/applications/almanac/xaction/AlmanacTransactionType.php new file mode 100644 index 0000000000..c7f58ec7bb --- /dev/null +++ b/src/applications/almanac/xaction/AlmanacTransactionType.php @@ -0,0 +1,97 @@ +getMetadataValue('almanac.property'); + $exists = $object->hasAlmanacProperty($property_key); + $value = $object->getAlmanacPropertyValue($property_key); + + return array( + 'existed' => $exists, + 'value' => $value, + ); + } + + protected function setAlmanacProperty($object, $value) { + $property_key = $this->getMetadataValue('almanac.property'); + + if ($object->hasAlmanacProperty($property_key)) { + $property = $object->getAlmanacProperty($property_key); + } else { + $property = id(new AlmanacProperty()) + ->setObjectPHID($object->getPHID()) + ->setFieldName($property_key); + } + + $property + ->setFieldValue($value) + ->save(); + } + + protected function deleteAlmanacProperty($object) { + $property_key = $this->getMetadataValue('almanac.property'); + if ($object->hasAlmanacProperty($property_key)) { + $property = $object->getAlmanacProperty($property_key); + $property->delete(); + } + } + + protected function getAlmanacSetPropertyTitle() { + $property_key = $this->getMetadataValue('almanac.property'); + + return pht( + '%s updated the property %s.', + $this->renderAuthor(), + $this->renderValue($property_key)); + } + + protected function getAlmanacDeletePropertyTitle() { + $property_key = $this->getMetadataValue('almanac.property'); + + return pht( + '%s removed the property %s.', + $this->renderAuthor(), + $this->renderValue($property_key)); + } + + protected function validateAlmanacSetPropertyTransactions( + $object, + array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $property_key = $xaction->getMetadataValue('almanac.property'); + + $message = null; + try { + AlmanacNames::validateName($property_key); + } catch (Exception $ex) { + $message = $ex->getMessage(); + } + + if ($message !== null) { + $errors[] = $this->newInvalidError($message, $xaction); + continue; + } + + $new_value = $xaction->getNewValue(); + try { + phutil_json_encode($new_value); + } catch (Exception $ex) { + $message = pht( + 'Almanac property values must be representable in JSON. %s', + $ex->getMessage()); + } + + if ($message !== null) { + $errors[] = $this->newInvalidError($message, $xaction); + continue; + } + } + + return $errors; + } + +} diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index 420d126507..e276e035e4 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -57,10 +57,6 @@ abstract class PhabricatorApplication abstract public function getName(); - public function getMenuName() { - return $this->getName(); - } - public function getShortDescription() { return pht('%s Application', $this->getName()); } diff --git a/src/applications/config/option/PhabricatorCoreConfigOptions.php b/src/applications/config/option/PhabricatorCoreConfigOptions.php index 0c9e69ebf2..08266217ea 100644 --- a/src/applications/config/option/PhabricatorCoreConfigOptions.php +++ b/src/applications/config/option/PhabricatorCoreConfigOptions.php @@ -268,24 +268,24 @@ EOREMARKUP if ($protocol !== 'http' && $protocol !== 'https') { throw new PhabricatorConfigValidationException( pht( - "Config option '%s' is invalid. The URI must start with ". - "%s' or '%s'.", + 'Config option "%s" is invalid. The URI must start with '. + '"%s" or "%s".', + $key, 'http://', - 'https://', - $key)); + 'https://')); } $domain = $uri->getDomain(); if (strpos($domain, '.') === false) { throw new PhabricatorConfigValidationException( pht( - "Config option '%s' is invalid. The URI must contain a dot ". - "('%s'), like '%s', not just a bare name like '%s'. Some web ". - "browsers will not set cookies on domains with no TLD.", + 'Config option "%s" is invalid. The URI must contain a dot '. + '("%s"), like "%s", not just a bare name like "%s". Some web '. + 'browsers will not set cookies on domains with no TLD.', + $key, '.', 'http://example.com/', - 'http://example/', - $key)); + 'http://example/')); } $path = $uri->getPath(); diff --git a/src/applications/differential/application/PhabricatorDifferentialApplication.php b/src/applications/differential/application/PhabricatorDifferentialApplication.php index da40739579..eaa4948eee 100644 --- a/src/applications/differential/application/PhabricatorDifferentialApplication.php +++ b/src/applications/differential/application/PhabricatorDifferentialApplication.php @@ -10,10 +10,6 @@ final class PhabricatorDifferentialApplication extends PhabricatorApplication { return pht('Differential'); } - public function getMenuName() { - return pht('Code Review'); - } - public function getShortDescription() { return pht('Pre-Commit Review'); } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 24f0fe7a07..50094b02f4 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -850,11 +850,13 @@ final class DifferentialTransactionEditor $revert_phids = array(); } - $this->setUnmentionablePHIDMap( - array_merge( - $task_phids, - $rev_phids, - $revert_phids)); + // See PHI574. Respect any unmentionable PHIDs which were set on the + // Editor by the caller. + $unmentionable_map = $this->getUnmentionablePHIDMap(); + $unmentionable_map += $task_phids; + $unmentionable_map += $rev_phids; + $unmentionable_map += $revert_phids; + $this->setUnmentionablePHIDMap($unmentionable_map); $result = array(); foreach ($edges as $type => $specs) { diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php index d42e58b747..70990bfe3c 100644 --- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php +++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php @@ -6,10 +6,6 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication { return pht('Diffusion'); } - public function getMenuName() { - return pht('Repositories'); - } - public function getShortDescription() { return pht('Host and Browse Repositories'); } @@ -57,6 +53,10 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication { 'history/(?P.*)' => 'DiffusionHistoryController', 'graph/(?P.*)' => 'DiffusionGraphController', 'browse/(?P.*)' => 'DiffusionBrowseController', + 'document/(?P.*)' + => 'DiffusionDocumentController', + 'blame/(?P.*)' + => 'DiffusionBlameController', 'lastmodified/(?P.*)' => 'DiffusionLastModifiedController', 'diff/' => 'DiffusionDiffController', 'tags/(?P.*)' => 'DiffusionTagListController', diff --git a/src/applications/diffusion/controller/DiffusionBlameController.php b/src/applications/diffusion/controller/DiffusionBlameController.php new file mode 100644 index 0000000000..66403a8af9 --- /dev/null +++ b/src/applications/diffusion/controller/DiffusionBlameController.php @@ -0,0 +1,266 @@ +loadDiffusionContext(); + if ($response) { + return $response; + } + + $viewer = $this->getViewer(); + $drequest = $this->getDiffusionRequest(); + $repository = $drequest->getRepository(); + + $blame = $this->loadBlame(); + + $identifiers = array_fuse($blame); + if ($identifiers) { + $commits = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->withRepository($repository) + ->withIdentifiers($identifiers) + ->execute(); + $commits = mpull($commits, null, 'getCommitIdentifier'); + } else { + $commits = array(); + } + + $commit_map = mpull($commits, 'getCommitIdentifier', 'getPHID'); + + $revisions = array(); + $revision_map = array(); + if ($commits) { + $revision_ids = id(new DifferentialRevision()) + ->loadIDsByCommitPHIDs(array_keys($commit_map)); + if ($revision_ids) { + $revisions = id(new DifferentialRevisionQuery()) + ->setViewer($viewer) + ->withIDs($revision_ids) + ->execute(); + $revisions = mpull($revisions, null, 'getID'); + } + + foreach ($revision_ids as $commit_phid => $revision_id) { + // If the viewer can't actually see this revision, skip it. + if (!isset($revisions[$revision_id])) { + continue; + } + $revision_map[$commit_map[$commit_phid]] = $revision_id; + } + } + + $base_href = (string)$drequest->generateURI( + array( + 'action' => 'browse', + 'stable' => true, + )); + + $skip_text = pht('Skip Past This Commit'); + $skip_icon = id(new PHUIIconView()) + ->setIcon('fa-backward'); + + Javelin::initBehavior('phabricator-tooltips'); + + $handle_phids = array(); + foreach ($commits as $commit) { + $author_phid = $commit->getAuthorPHID(); + if ($author_phid) { + $handle_phids[] = $author_phid; + } + } + + foreach ($revisions as $revision) { + $handle_phids[] = $revision->getAuthorPHID(); + } + + $handles = $viewer->loadHandles($handle_phids); + + + $map = array(); + $epochs = array(); + foreach ($identifiers as $identifier) { + $revision_id = idx($revision_map, $identifier); + if ($revision_id) { + $revision = idx($revisions, $revision_id); + } else { + $revision = null; + } + + $skip_href = $base_href.'?before='.$identifier; + + $skip_link = javelin_tag( + 'a', + array( + 'href' => $skip_href, + 'sigil' => 'has-tooltip', + 'meta' => array( + 'tip' => $skip_text, + 'align' => 'E', + 'size' => 300, + ), + ), + $skip_icon); + + $commit = $commits[$identifier]; + + $author_phid = $commit->getAuthorPHID(); + if (!$author_phid && $revision) { + $author_phid = $revision->getAuthorPHID(); + } + + if (!$author_phid) { + // This means we couldn't identify an author for the commit or the + // revision. We just render a blank for alignment. + $author_style = null; + $author_href = null; + $author_sigil = null; + $author_meta = null; + } else { + $author_src = $handles[$author_phid]->getImageURI(); + $author_style = 'background-image: url('.$author_src.');'; + $author_href = $handles[$author_phid]->getURI(); + $author_sigil = 'has-tooltip'; + $author_meta = array( + 'tip' => $handles[$author_phid]->getName(), + 'align' => 'E', + ); + } + + $author_link = javelin_tag( + $author_href ? 'a' : 'span', + array( + 'class' => 'phabricator-source-blame-author', + 'style' => $author_style, + 'href' => $author_href, + 'sigil' => $author_sigil, + 'meta' => $author_meta, + )); + + $commit_link = javelin_tag( + 'a', + array( + 'href' => $commit->getURI(), + 'sigil' => 'has-tooltip', + 'meta' => array( + 'tip' => $this->renderCommitTooltip($commit, $handles), + 'align' => 'E', + 'size' => 600, + ), + ), + $commit->getLocalName()); + + $info = array( + $author_link, + $commit_link, + ); + + if ($revision) { + $revision_link = javelin_tag( + 'a', + array( + 'href' => $revision->getURI(), + 'sigil' => 'has-tooltip', + 'meta' => array( + 'tip' => $this->renderRevisionTooltip($revision, $handles), + 'align' => 'E', + 'size' => 600, + ), + ), + $revision->getMonogram()); + + $info = array( + $info, + " \xC2\xB7 ", + $revision_link, + ); + } + + $epoch = $commit->getEpoch(); + $epochs[] = $epoch; + + $data = array( + 'skip' => $skip_link, + 'info' => hsprintf('%s', $info), + 'epoch' => $epoch, + ); + + $map[$identifier] = $data; + } + + $epoch_min = min($epochs); + $epoch_max = max($epochs); + + return id(new AphrontAjaxResponse())->setContent( + array( + 'blame' => $blame, + 'map' => $map, + 'epoch' => array( + 'min' => $epoch_min, + 'max' => $epoch_max, + ), + )); + } + + private function loadBlame() { + $drequest = $this->getDiffusionRequest(); + + $commit = $drequest->getCommit(); + $path = $drequest->getPath(); + + $blame_timeout = 15; + + $blame = $this->callConduitWithDiffusionRequest( + 'diffusion.blame', + array( + 'commit' => $commit, + 'paths' => array($path), + 'timeout' => $blame_timeout, + )); + + return idx($blame, $path, array()); + } + + private function renderRevisionTooltip( + DifferentialRevision $revision, + $handles) { + $viewer = $this->getViewer(); + + $date = phabricator_date($revision->getDateModified(), $viewer); + $monogram = $revision->getMonogram(); + $title = $revision->getTitle(); + $header = "{$monogram} {$title}"; + + $author = $handles[$revision->getAuthorPHID()]->getName(); + + return "{$header}\n{$date} \xC2\xB7 {$author}"; + } + + private function renderCommitTooltip( + PhabricatorRepositoryCommit $commit, + $handles) { + + $viewer = $this->getViewer(); + + $date = phabricator_date($commit->getEpoch(), $viewer); + $summary = trim($commit->getSummary()); + + $author_phid = $commit->getAuthorPHID(); + if ($author_phid && isset($handles[$author_phid])) { + $author_name = $handles[$author_phid]->getName(); + } else { + $author_name = null; + } + + if ($author_name) { + return "{$summary}\n{$date} \xC2\xB7 {$author_name}"; + } else { + return "{$summary}\n{$date}"; + } + } + +} diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index a2380aab4a..1053ef477c 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -45,13 +45,13 @@ final class DiffusionBrowseController extends DiffusionController { if ($is_file) { return $this->browseFile(); - } else { - $paths = $results->getPaths(); - $paths = $pager->sliceResults($paths); - $results->setPaths($paths); - - return $this->browseDirectory($results, $pager); } + + $paths = $results->getPaths(); + $paths = $pager->sliceResults($paths); + $results->setPaths($paths); + + return $this->browseDirectory($results, $pager); } private function browseSearch() { @@ -110,42 +110,13 @@ final class DiffusionBrowseController extends DiffusionController { } $path = $drequest->getPath(); - - $blame_key = PhabricatorDiffusionBlameSetting::SETTINGKEY; - $show_blame = $request->getBool( - 'blame', - $viewer->getUserSetting($blame_key)); - - $view = $request->getStr('view'); - if ($request->isFormPost() && $view != 'raw' && $viewer->isLoggedIn()) { - $preferences = PhabricatorUserPreferences::loadUserPreferences($viewer); - - $editor = id(new PhabricatorUserPreferencesEditor()) - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true); - - $xactions = array(); - $xactions[] = $preferences->newTransaction($blame_key, $show_blame); - $editor->applyTransactions($preferences, $xactions); - - $uri = $request->getRequestURI() - ->alter('blame', null); - - return id(new AphrontRedirectResponse())->setURI($uri); - } - - // We need the blame information if blame is on and this is an Ajax request. - // If blame is on and this is a colorized request, we don't show blame at - // first (we ajax it in afterward) so we don't need to query for it. - $needs_blame = ($show_blame && $request->isAjax()); - $params = array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), ); + $view = $request->getStr('view'); + $byte_limit = null; if ($view !== 'raw') { $byte_limit = PhabricatorFileStorageEngine::getChunkThreshold(); @@ -207,41 +178,27 @@ final class DiffusionBrowseController extends DiffusionController { $file->setName($basename); return $file->getRedirectResponse(); - } else { - $corpus = $this->buildGitLFSCorpus($lfs_ref); } - } else if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) { - $file_uri = $file->getBestURI(); - if ($file->isViewableImage()) { - $corpus = $this->buildImageCorpus($file_uri); - } else { - $corpus = $this->buildBinaryCorpus($file_uri, $data); - } + $corpus = $this->buildGitLFSCorpus($lfs_ref); } else { - $this->loadLintMessages(); $this->coverage = $drequest->loadCoverage(); $show_editor = true; - // Build the content of the file. - $corpus = $this->buildCorpus( - $show_blame, - $data, - $needs_blame, - $drequest, - $path, - $data); + $ref = id(new PhabricatorDocumentRef()) + ->setFile($file); + + $engine = id(new DiffusionDocumentRenderingEngine()) + ->setRequest($request) + ->setDiffusionRequest($drequest); + + $corpus = $engine->newDocumentView($ref); + + $this->corpusButtons[] = $this->renderFileButton(); } } - if ($request->isAjax()) { - return id(new AphrontAjaxResponse())->setContent($corpus); - } - - require_celerity_resource('diffusion-source-css'); - - // Render the page. - $bar = $this->buildButtonBar($drequest, $show_blame, $show_editor); + $bar = $this->buildButtonBar($drequest, $show_editor); $header = $this->buildHeaderView($drequest); $header->setHeaderIcon('fa-file-code-o'); @@ -510,198 +467,8 @@ final class DiffusionBrowseController extends DiffusionController { return $view; } - private function loadLintMessages() { - $drequest = $this->getDiffusionRequest(); - $branch = $drequest->loadBranch(); - - if (!$branch || !$branch->getLintCommit()) { - return; - } - - $this->lintCommit = $branch->getLintCommit(); - - $conn = id(new PhabricatorRepository())->establishConnection('r'); - - $where = ''; - if ($drequest->getLint()) { - $where = qsprintf( - $conn, - 'AND code = %s', - $drequest->getLint()); - } - - $this->lintMessages = queryfx_all( - $conn, - 'SELECT * FROM %T WHERE branchID = %d %Q AND path = %s', - PhabricatorRepository::TABLE_LINTMESSAGE, - $branch->getID(), - $where, - '/'.$drequest->getPath()); - } - - private function buildCorpus( - $show_blame, - $file_corpus, - $needs_blame, - DiffusionRequest $drequest, - $path, - $data) { - - $viewer = $this->getViewer(); - $blame_timeout = 15; - $blame_failed = false; - - $highlight_limit = DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT; - $blame_limit = DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT; - $can_highlight = (strlen($file_corpus) <= $highlight_limit); - $can_blame = (strlen($file_corpus) <= $blame_limit); - - if ($needs_blame && $can_blame) { - $blame = $this->loadBlame($path, $drequest->getCommit(), $blame_timeout); - list($blame_list, $blame_commits) = $blame; - if ($blame_list === null) { - $blame_failed = true; - $blame_list = array(); - } - } else { - $blame_list = array(); - $blame_commits = array(); - } - - require_celerity_resource('syntax-highlighting-css'); - if ($can_highlight) { - $highlighted = PhabricatorSyntaxHighlighter::highlightWithFilename( - $path, - $file_corpus); - } else { - // Highlight as plain text to escape the content properly. - $highlighted = PhabricatorSyntaxHighlighter::highlightWithLanguage( - 'txt', - $file_corpus); - } - - $lines = phutil_split_lines($highlighted); - - $rows = $this->buildDisplayRows( - $lines, - $blame_list, - $blame_commits, - $show_blame); - - $corpus_table = javelin_tag( - 'table', - array( - 'class' => 'diffusion-source remarkup-code PhabricatorMonospaced', - 'sigil' => 'phabricator-source', - 'meta' => array( - 'uri' => $this->getLineNumberBaseURI(), - ), - ), - $rows); - - $corpus_table = phutil_tag_div('diffusion-source-wrap', $corpus_table); - - if ($this->getRequest()->isAjax()) { - return $corpus_table; - } - - $id = celerity_generate_unique_node_id(); - - $repo = $drequest->getRepository(); - $symbol_repos = nonempty($repo->getSymbolSources(), array()); - $symbol_repos[] = $repo->getPHID(); - - $lang = last(explode('.', $drequest->getPath())); - $repo_languages = $repo->getSymbolLanguages(); - $repo_languages = nonempty($repo_languages, array()); - $repo_languages = array_fill_keys($repo_languages, true); - - $needs_symbols = true; - if ($repo_languages && $symbol_repos) { - $have_symbols = id(new DiffusionSymbolQuery()) - ->existsSymbolsInRepository($repo->getPHID()); - if (!$have_symbols) { - $needs_symbols = false; - } - } - - if ($needs_symbols && $repo_languages) { - $needs_symbols = isset($repo_languages[$lang]); - } - - if ($needs_symbols) { - Javelin::initBehavior( - 'repository-crossreference', - array( - 'container' => $id, - 'lang' => $lang, - 'repositories' => $symbol_repos, - )); - } - - $corpus = phutil_tag( - 'div', - array( - 'id' => $id, - ), - $corpus_table); - - Javelin::initBehavior('load-blame', array('id' => $id)); - - $this->corpusButtons[] = $this->renderFileButton(); - $title = basename($this->getDiffusionRequest()->getPath()); - $icon = 'fa-file-code-o'; - $drequest = $this->getDiffusionRequest(); - $this->buildActionButtons($drequest); - - $header = $this->buildPanelHeaderView($title, $icon); - - $corpus = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($corpus) - ->addClass('diffusion-mobile-view') - ->addSigil('diffusion-file-content-view') - ->setMetadata( - array( - 'path' => $this->getDiffusionRequest()->getPath(), - )) - ->setCollapsed(true); - - $messages = array(); - - if (!$can_highlight) { - $messages[] = pht( - 'This file is larger than %s, so syntax highlighting is disabled '. - 'by default.', - phutil_format_bytes($highlight_limit)); - } - - if ($show_blame && !$can_blame) { - $messages[] = pht( - 'This file is larger than %s, so blame is disabled.', - phutil_format_bytes($blame_limit)); - } - - if ($blame_failed) { - $messages[] = pht( - 'Failed to load blame information for this file in %s second(s).', - new PhutilNumber($blame_timeout)); - } - - if ($messages) { - $corpus->setInfoView( - id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_WARNING) - ->setErrors($messages)); - } - - return $corpus; - } - private function buildButtonBar( DiffusionRequest $drequest, - $show_blame, $show_editor) { $viewer = $this->getViewer(); @@ -728,38 +495,6 @@ final class DiffusionBrowseController extends DiffusionController { ))) ->setIcon('fa-backward'); - if ($show_blame) { - $blame_text = pht('Disable Blame'); - $blame_icon = 'fa-exclamation-circle lightgreytext'; - $blame_value = 0; - } else { - $blame_text = pht('Enable Blame'); - $blame_icon = 'fa-exclamation-circle'; - $blame_value = 1; - } - - $blame = id(new PHUIButtonView()) - ->setText($blame_text) - ->setIcon($blame_icon) - ->setUser($viewer) - ->setSelected(!$blame_value) - ->setColor(PHUIButtonView::GREY); - - if ($viewer->isLoggedIn()) { - $blame = phabricator_form( - $viewer, - array( - 'action' => $base_uri->alter('blame', $blame_value), - 'method' => 'POST', - 'style' => 'display: inline-block;', - ), - $blame); - } else { - $blame->setTag('a'); - $blame->setHref($base_uri->alter('blame', $blame_value)); - } - $buttons[] = $blame; - if ($editor_link) { $buttons[] = id(new PHUIButtonView()) @@ -773,33 +508,6 @@ final class DiffusionBrowseController extends DiffusionController { ->setColor(PHUIButtonView::GREY); } - $href = null; - $show_lint = true; - if ($this->getRequest()->getStr('lint') !== null) { - $lint_text = pht('Hide Lint'); - $href = $base_uri->alter('lint', null); - - } else if ($this->lintCommit === null) { - $show_lint = false; - } else { - $lint_text = pht('Show Lint'); - $href = $this->getDiffusionRequest()->generateURI(array( - 'action' => 'browse', - 'commit' => $this->lintCommit, - ))->alter('lint', ''); - } - - if ($show_lint) { - $buttons[] = - id(new PHUIButtonView()) - ->setTag('a') - ->setText($lint_text) - ->setHref($href) - ->setIcon('fa-exclamation-triangle') - ->setDisabled(!$href) - ->setColor(PHUIButtonView::GREY); - } - $bar = id(new PHUILeftRightView()) ->setLeft($buttons) ->addClass('diffusion-action-bar full-mobile-buttons'); @@ -928,476 +636,6 @@ final class DiffusionBrowseController extends DiffusionController { ->setColor(PHUIButtonView::GREY); } - private function buildDisplayRows( - array $lines, - array $blame_list, - array $blame_commits, - $show_blame) { - - $request = $this->getRequest(); - $viewer = $this->getViewer(); - $drequest = $this->getDiffusionRequest(); - $repository = $drequest->getRepository(); - - $revision_map = array(); - $revisions = array(); - if ($blame_commits) { - $commit_map = mpull($blame_commits, 'getCommitIdentifier', 'getPHID'); - - $revision_ids = id(new DifferentialRevision()) - ->loadIDsByCommitPHIDs(array_keys($commit_map)); - if ($revision_ids) { - $revisions = id(new DifferentialRevisionQuery()) - ->setViewer($viewer) - ->withIDs($revision_ids) - ->execute(); - $revisions = mpull($revisions, null, 'getID'); - } - - foreach ($revision_ids as $commit_phid => $revision_id) { - // If the viewer can't actually see this revision, skip it. - if (!isset($revisions[$revision_id])) { - continue; - } - $revision_map[$commit_map[$commit_phid]] = $revision_id; - } - } - - $phids = array(); - foreach ($blame_commits as $commit) { - $author_phid = $commit->getAuthorPHID(); - if ($author_phid === null) { - continue; - } - $phids[$author_phid] = $author_phid; - } - - foreach ($revisions as $revision) { - $author_phid = $revision->getAuthorPHID(); - if ($author_phid === null) { - continue; - } - $phids[$author_phid] = $author_phid; - } - - $handles = $viewer->loadHandles($phids); - - $author_phids = array(); - $author_map = array(); - foreach ($blame_commits as $commit) { - $commit_identifier = $commit->getCommitIdentifier(); - - $author_phid = ''; - if (isset($revision_map[$commit_identifier])) { - $revision_id = $revision_map[$commit_identifier]; - $revision = $revisions[$revision_id]; - $author_phid = $revision->getAuthorPHID(); - } else { - $author_phid = $commit->getAuthorPHID(); - } - - $author_map[$commit_identifier] = $author_phid; - $author_phids[$author_phid] = $author_phid; - } - - $colors = array(); - if ($blame_commits) { - $epochs = array(); - - foreach ($blame_commits as $identifier => $commit) { - $epochs[$identifier] = $commit->getEpoch(); - } - - $epoch_list = array_filter($epochs); - $epoch_list = array_unique($epoch_list); - $epoch_list = array_values($epoch_list); - - $epoch_min = min($epoch_list); - $epoch_max = max($epoch_list); - $epoch_range = ($epoch_max - $epoch_min) + 1; - - foreach ($blame_commits as $identifier => $commit) { - $epoch = $epochs[$identifier]; - if (!$epoch) { - $color = '#ffffdd'; // Warning color, missing data. - } else { - $color_ratio = ($epoch - $epoch_min) / $epoch_range; - $color_value = 0xE6 * (1.0 - $color_ratio); - $color = sprintf( - '#%02x%02x%02x', - $color_value, - 0xF6, - $color_value); - } - - $colors[$identifier] = $color; - } - } - - $display = array(); - $last_identifier = null; - $last_color = null; - foreach ($lines as $line_index => $line) { - $color = '#f6f6f6'; - $duplicate = false; - if (isset($blame_list[$line_index])) { - $identifier = $blame_list[$line_index]; - if (isset($colors[$identifier])) { - $color = $colors[$identifier]; - } - - if ($identifier === $last_identifier) { - $duplicate = true; - } else { - $last_identifier = $identifier; - } - } - - $display[$line_index] = array( - 'data' => $line, - 'target' => false, - 'highlighted' => false, - 'color' => $color, - 'duplicate' => $duplicate, - ); - } - - $line_arr = array(); - $line_str = $drequest->getLine(); - $ranges = explode(',', $line_str); - foreach ($ranges as $range) { - if (strpos($range, '-') !== false) { - list($min, $max) = explode('-', $range, 2); - $line_arr[] = array( - 'min' => min($min, $max), - 'max' => max($min, $max), - ); - } else if (strlen($range)) { - $line_arr[] = array( - 'min' => $range, - 'max' => $range, - ); - } - } - - // Mark the first highlighted line as the target line. - if ($line_arr) { - $target_line = $line_arr[0]['min']; - if (isset($display[$target_line - 1])) { - $display[$target_line - 1]['target'] = true; - } - } - - // Mark all other highlighted lines as highlighted. - foreach ($line_arr as $range) { - for ($ii = $range['min']; $ii <= $range['max']; $ii++) { - if (isset($display[$ii - 1])) { - $display[$ii - 1]['highlighted'] = true; - } - } - } - - $engine = null; - $inlines = array(); - if ($this->getRequest()->getStr('lint') !== null && $this->lintMessages) { - $engine = new PhabricatorMarkupEngine(); - $engine->setViewer($viewer); - - foreach ($this->lintMessages as $message) { - $inline = id(new PhabricatorAuditInlineComment()) - ->setSyntheticAuthor( - ArcanistLintSeverity::getStringForSeverity($message['severity']). - ' '.$message['code'].' ('.$message['name'].')') - ->setLineNumber($message['line']) - ->setContent($message['description']); - $inlines[$message['line']][] = $inline; - - $engine->addObject( - $inline, - PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); - } - - $engine->process(); - require_celerity_resource('differential-changeset-view-css'); - } - - $rows = $this->renderInlines( - idx($inlines, 0, array()), - $show_blame, - (bool)$this->coverage, - $engine); - - // NOTE: We're doing this manually because rendering is otherwise - // dominated by URI generation for very large files. - $line_base = $this->getLineNumberBaseURI(); - - require_celerity_resource('aphront-tooltip-css'); - Javelin::initBehavior('phabricator-oncopy'); - Javelin::initBehavior('phabricator-tooltips'); - Javelin::initBehavior('phabricator-line-linker'); - - // Render these once, since they tend to get repeated many times in large - // blame outputs. - $commit_links = $this->renderCommitLinks($blame_commits, $handles); - $revision_links = $this->renderRevisionLinks($revisions, $handles); - $author_links = $this->renderAuthorLinks($author_map, $handles); - - if ($this->coverage) { - require_celerity_resource('differential-changeset-view-css'); - Javelin::initBehavior( - 'diffusion-browse-file', - array( - 'labels' => array( - 'cov-C' => pht('Covered'), - 'cov-N' => pht('Not Covered'), - 'cov-U' => pht('Not Executable'), - ), - )); - } - - $skip_text = pht('Skip Past This Commit'); - $skip_icon = id(new PHUIIconView()) - ->setIcon('fa-caret-square-o-left'); - - foreach ($display as $line_index => $line) { - $row = array(); - - $line_number = $line_index + 1; - $line_href = $line_base.'$'.$line_number; - - if (isset($blame_list[$line_index])) { - $identifier = $blame_list[$line_index]; - } else { - $identifier = null; - } - - $revision_link = null; - $commit_link = null; - $author_link = null; - $before_link = null; - - $style = 'background: '.$line['color'].';'; - - if ($identifier && !$line['duplicate']) { - if (isset($commit_links[$identifier])) { - $commit_link = $commit_links[$identifier]; - $author_link = $author_links[$author_map[$identifier]]; - } - - if (isset($revision_map[$identifier])) { - $revision_id = $revision_map[$identifier]; - if (isset($revision_links[$revision_id])) { - $revision_link = $revision_links[$revision_id]; - } - } - - $skip_href = $line_href.'?before='.$identifier.'&view=blame'; - $before_link = javelin_tag( - 'a', - array( - 'href' => $skip_href, - 'sigil' => 'has-tooltip', - 'meta' => array( - 'tip' => $skip_text, - 'align' => 'E', - 'size' => 300, - ), - ), - $skip_icon); - } - - if ($show_blame) { - $row[] = phutil_tag( - 'th', - array( - 'class' => 'diffusion-blame-link', - ), - $before_link); - - $object_links = array(); - $object_links[] = $author_link; - $object_links[] = $commit_link; - if ($revision_link) { - $object_links[] = phutil_tag('span', array(), '/'); - $object_links[] = $revision_link; - } - - $row[] = phutil_tag( - 'th', - array( - 'class' => 'diffusion-rev-link', - ), - $object_links); - } - - $line_link = phutil_tag( - 'a', - array( - 'href' => $line_href, - 'style' => $style, - ), - $line_number); - - $row[] = javelin_tag( - 'th', - array( - 'class' => 'diffusion-line-link', - 'sigil' => 'phabricator-source-line', - 'style' => $style, - ), - $line_link); - - if ($line['target']) { - Javelin::initBehavior( - 'diffusion-jump-to', - array( - 'target' => 'scroll_target', - )); - $anchor_text = phutil_tag( - 'a', - array( - 'id' => 'scroll_target', - ), - ''); - } else { - $anchor_text = null; - } - - $row[] = phutil_tag( - 'td', - array( - ), - array( - $anchor_text, - - // NOTE: See phabricator-oncopy behavior. - "\xE2\x80\x8B", - - // TODO: [HTML] Not ideal. - phutil_safe_html(str_replace("\t", ' ', $line['data'])), - )); - - if ($this->coverage) { - $cov_index = $line_index; - - if (isset($this->coverage[$cov_index])) { - $cov_class = $this->coverage[$cov_index]; - } else { - $cov_class = 'N'; - } - - $row[] = phutil_tag( - 'td', - array( - 'class' => 'cov cov-'.$cov_class, - ), - ''); - } - - $rows[] = phutil_tag( - 'tr', - array( - 'class' => ($line['highlighted'] ? - 'phabricator-source-highlight' : - null), - ), - $row); - - $cur_inlines = $this->renderInlines( - idx($inlines, $line_number, array()), - $show_blame, - $this->coverage, - $engine); - foreach ($cur_inlines as $cur_inline) { - $rows[] = $cur_inline; - } - } - - return $rows; - } - - private function renderInlines( - array $inlines, - $show_blame, - $has_coverage, - $engine) { - - $rows = array(); - foreach ($inlines as $inline) { - - // TODO: This should use modern scaffolding code. - - $inline_view = id(new PHUIDiffInlineCommentDetailView()) - ->setUser($this->getViewer()) - ->setMarkupEngine($engine) - ->setInlineComment($inline) - ->render(); - - $row = array_fill(0, ($show_blame ? 3 : 1), phutil_tag('th')); - - $row[] = phutil_tag('td', array(), $inline_view); - - if ($has_coverage) { - $row[] = phutil_tag( - 'td', - array( - 'class' => 'cov cov-I', - )); - } - - $rows[] = phutil_tag('tr', array('class' => 'inline'), $row); - } - - return $rows; - } - - private function buildImageCorpus($file_uri) { - $properties = new PHUIPropertyListView(); - - $properties->addImageContent( - phutil_tag( - 'img', - array( - 'src' => $file_uri, - ))); - - $this->corpusButtons[] = $this->renderFileButton($file_uri); - $title = basename($this->getDiffusionRequest()->getPath()); - $icon = 'fa-file-image-o'; - $drequest = $this->getDiffusionRequest(); - $this->buildActionButtons($drequest); - $header = $this->buildPanelHeaderView($title, $icon); - - return id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->addClass('diffusion-mobile-view') - ->addPropertyList($properties); - } - - private function buildBinaryCorpus($file_uri, $data) { - $size = new PhutilNumber(strlen($data)); - $text = pht('This is a binary file. It is %s byte(s) in length.', $size); - $text = id(new PHUIBoxView()) - ->addPadding(PHUI::PADDING_LARGE) - ->appendChild($text); - - $this->corpusButtons[] = $this->renderFileButton($file_uri); - $title = basename($this->getDiffusionRequest()->getPath()); - $icon = 'fa-file'; - $drequest = $this->getDiffusionRequest(); - $this->buildActionButtons($drequest); - $header = $this->buildPanelHeaderView($title, $icon); - - $box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->addClass('diffusion-mobile-view') - ->appendChild($text); - - return $box; - } - private function buildErrorCorpus($message) { $text = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE) @@ -1557,33 +795,6 @@ final class DiffusionBrowseController extends DiffusionController { return head($parents); } - private function renderRevisionTooltip( - DifferentialRevision $revision, - $handles) { - $viewer = $this->getRequest()->getUser(); - - $date = phabricator_date($revision->getDateModified(), $viewer); - $id = $revision->getID(); - $title = $revision->getTitle(); - $header = "D{$id} {$title}"; - - $author = $handles[$revision->getAuthorPHID()]->getName(); - - return "{$header}\n{$date} \xC2\xB7 {$author}"; - } - - private function renderCommitTooltip( - PhabricatorRepositoryCommit $commit, - $author) { - - $viewer = $this->getRequest()->getUser(); - - $date = phabricator_date($commit->getEpoch(), $viewer); - $summary = trim($commit->getSummary()); - - return "{$summary}\n{$date} \xC2\xB7 {$author}"; - } - protected function markupText($text) { $engine = PhabricatorMarkupEngine::newDiffusionMarkupEngine(); $engine->setConfig('viewer', $this->getRequest()->getUser()); @@ -1767,127 +978,6 @@ final class DiffusionBrowseController extends DiffusionController { return $view; } - private function loadBlame($path, $commit, $timeout) { - $blame = $this->callConduitWithDiffusionRequest( - 'diffusion.blame', - array( - 'commit' => $commit, - 'paths' => array($path), - 'timeout' => $timeout, - )); - - $identifiers = idx($blame, $path, null); - - if ($identifiers) { - $viewer = $this->getViewer(); - $drequest = $this->getDiffusionRequest(); - $repository = $drequest->getRepository(); - - $commits = id(new DiffusionCommitQuery()) - ->setViewer($viewer) - ->withRepository($repository) - ->withIdentifiers($identifiers) - // TODO: We only fetch this to improve author display behavior, but - // shouldn't really need to? - ->needCommitData(true) - ->execute(); - $commits = mpull($commits, null, 'getCommitIdentifier'); - } else { - $commits = array(); - } - - return array($identifiers, $commits); - } - - private function renderAuthorLinks(array $authors, $handles) { - $links = array(); - - foreach ($authors as $phid) { - if (!strlen($phid)) { - // This means we couldn't identify an author for the commit or the - // revision. We just render a blank for alignment. - $style = null; - $href = null; - $sigil = null; - $meta = null; - } else { - $src = $handles[$phid]->getImageURI(); - $style = 'background-image: url('.$src.');'; - $href = $handles[$phid]->getURI(); - $sigil = 'has-tooltip'; - $meta = array( - 'tip' => $handles[$phid]->getName(), - 'align' => 'E', - ); - } - - $links[$phid] = javelin_tag( - $href ? 'a' : 'span', - array( - 'class' => 'diffusion-author-link', - 'style' => $style, - 'href' => $href, - 'sigil' => $sigil, - 'meta' => $meta, - )); - } - - return $links; - } - - private function renderCommitLinks(array $commits, $handles) { - $links = array(); - foreach ($commits as $identifier => $commit) { - $tooltip = $this->renderCommitTooltip( - $commit, - $commit->renderAuthorShortName($handles)); - - $commit_link = javelin_tag( - 'a', - array( - 'href' => $commit->getURI(), - 'sigil' => 'has-tooltip', - 'meta' => array( - 'tip' => $tooltip, - 'align' => 'E', - 'size' => 600, - ), - ), - $commit->getLocalName()); - - $links[$identifier] = $commit_link; - } - - return $links; - } - - private function renderRevisionLinks(array $revisions, $handles) { - $links = array(); - - foreach ($revisions as $revision) { - $revision_id = $revision->getID(); - - $tooltip = $this->renderRevisionTooltip($revision, $handles); - - $revision_link = javelin_tag( - 'a', - array( - 'href' => '/'.$revision->getMonogram(), - 'sigil' => 'has-tooltip', - 'meta' => array( - 'tip' => $tooltip, - 'align' => 'E', - 'size' => 600, - ), - ), - $revision->getMonogram()); - - $links[$revision_id] = $revision_link; - } - - return $links; - } - private function getGitLFSRef(PhabricatorRepository $repository, $data) { if (!$repository->canUseGitLFS()) { return null; @@ -2034,13 +1124,4 @@ final class DiffusionBrowseController extends DiffusionController { ->setTable($history_table); } - private function getLineNumberBaseURI() { - $drequest = $this->getDiffusionRequest(); - - return (string)$drequest->generateURI( - array( - 'action' => 'browse', - 'stable' => true, - )); - } } diff --git a/src/applications/diffusion/controller/DiffusionDocumentController.php b/src/applications/diffusion/controller/DiffusionDocumentController.php new file mode 100644 index 0000000000..c8956fc84c --- /dev/null +++ b/src/applications/diffusion/controller/DiffusionDocumentController.php @@ -0,0 +1,45 @@ +loadDiffusionContext(); + if ($response) { + return $response; + } + + $drequest = $this->getDiffusionRequest(); + + $engine = id(new DiffusionDocumentRenderingEngine()) + ->setRequest($request) + ->setDiffusionRequest($drequest) + ->setController($this); + + $viewer = $this->getViewer(); + $request = $this->getRequest(); + $repository = $drequest->getRepository(); + + $file_phid = $request->getStr('filePHID'); + + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($file_phid)) + ->executeOne(); + if (!$file) { + return $engine->newErrorResponse( + pht( + 'This file ("%s") does not exist or could not be loaded.', + $file_phid)); + } + + $ref = id(new PhabricatorDocumentRef()) + ->setFile($file); + + return $engine->newRenderResponse($ref); + } + +} diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index ac0b993b2f..5a5c446a29 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -431,10 +431,13 @@ final class DiffusionServeController extends DiffusionController { $uri = $repository->getAlmanacServiceURI( $viewer, - $is_cluster_request, array( - 'http', - 'https', + 'neverProxy' => $is_cluster_request, + 'protocols' => array( + 'http', + 'https', + ), + 'writable' => !$this->isReadOnlyRequest($repository), )); if ($uri) { $future = $this->getRequest()->newClusterProxyFuture($uri); diff --git a/src/applications/diffusion/document/DiffusionDocumentRenderingEngine.php b/src/applications/diffusion/document/DiffusionDocumentRenderingEngine.php new file mode 100644 index 0000000000..17abba5c4f --- /dev/null +++ b/src/applications/diffusion/document/DiffusionDocumentRenderingEngine.php @@ -0,0 +1,102 @@ +diffusionRequest = $drequest; + return $this; + } + + public function getDiffusionRequest() { + return $this->diffusionRequest; + } + + protected function newRefViewURI( + PhabricatorDocumentRef $ref, + PhabricatorDocumentEngine $engine) { + + $file = $ref->getFile(); + $engine_key = $engine->getDocumentEngineKey(); + $drequest = $this->getDiffusionRequest(); + + return (string)$drequest->generateURI( + array( + 'action' => 'browse', + 'stable' => true, + 'params' => array( + 'as' => $engine_key, + ), + )); + } + + protected function newRefRenderURI( + PhabricatorDocumentRef $ref, + PhabricatorDocumentEngine $engine) { + + $engine_key = $engine->getDocumentEngineKey(); + + $file = $ref->getFile(); + $file_phid = $file->getPHID(); + + $drequest = $this->getDiffusionRequest(); + + return (string)$drequest->generateURI( + array( + 'action' => 'document', + 'stable' => true, + 'params' => array( + 'as' => $engine_key, + 'filePHID' => $file_phid, + ), + )); + } + + protected function getSelectedDocumentEngineKey() { + return $this->getRequest()->getStr('as'); + } + + protected function getSelectedLineRange() { + $range = $this->getDiffusionRequest()->getLine(); + return AphrontRequest::parseURILineRange($range, 1000); + } + + protected function addApplicationCrumbs( + PHUICrumbsView $crumbs, + PhabricatorDocumentRef $ref = null) { + return; + } + + protected function willRenderRef(PhabricatorDocumentRef $ref) { + $drequest = $this->getDiffusionRequest(); + + $blame_uri = (string)$drequest->generateURI( + array( + 'action' => 'blame', + 'stable' => true, + )); + + $ref + ->setSymbolMetadata($this->getSymbolMetadata()) + ->setBlameURI($blame_uri); + } + + private function getSymbolMetadata() { + $drequest = $this->getDiffusionRequest(); + + $repo = $drequest->getRepository(); + $symbol_repos = nonempty($repo->getSymbolSources(), array()); + $symbol_repos[] = $repo->getPHID(); + + $lang = last(explode('.', $drequest->getPath())); + + return array( + 'repositories' => $symbol_repos, + 'lang' => $lang, + 'path' => $drequest->getPath(), + ); + } + +} diff --git a/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php index 400340abeb..d48abc159e 100644 --- a/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php @@ -29,7 +29,7 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow { ->setLog($this); if ($this->shouldProxy()) { - $command = $this->getProxyCommand(); + $command = $this->getProxyCommand(true); $did_synchronize = false; if ($device) { diff --git a/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php index 5ed42ba79d..e96030f577 100644 --- a/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php @@ -22,7 +22,7 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow { $is_proxy = $this->shouldProxy(); if ($is_proxy) { - $command = $this->getProxyCommand(); + $command = $this->getProxyCommand(false); if ($device) { $this->writeClusterEngineLogMessage( diff --git a/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php index 5701a4bac1..221d1696b2 100644 --- a/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php @@ -45,7 +45,10 @@ final class DiffusionMercurialServeSSHWorkflow } if ($this->shouldProxy()) { - $command = $this->getProxyCommand(); + // NOTE: For now, we're always requesting a writable node. The request + // may not actually need one, but we can't currently determine whether + // it is read-only or not at this phase of evaluation. + $command = $this->getProxyCommand(true); } else { $command = csprintf( 'hg -R %s serve --stdio', diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php index baf1749252..02cacbfe94 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php @@ -5,7 +5,7 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { private $args; private $repository; private $hasWriteAccess; - private $proxyURI; + private $shouldProxy; private $baseRequestPath; public function getRepository() { @@ -69,18 +69,34 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { return php_uname('n'); } - protected function getTargetDeviceName() { - // TODO: This should use the correct device identity. - $uri = new PhutilURI($this->proxyURI); - return $uri->getDomain(); - } - protected function shouldProxy() { - return (bool)$this->proxyURI; + return $this->shouldProxy; } - protected function getProxyCommand() { - $uri = new PhutilURI($this->proxyURI); + protected function getProxyCommand($for_write) { + $viewer = $this->getSSHUser(); + $repository = $this->getRepository(); + + $is_cluster_request = $this->getIsClusterRequest(); + + $uri = $repository->getAlmanacServiceURI( + $viewer, + array( + 'neverProxy' => $is_cluster_request, + 'protocols' => array( + 'ssh', + ), + 'writable' => $for_write, + )); + + if (!$uri) { + throw new Exception( + pht( + 'Failed to generate an intracluster proxy URI even though this '. + 'request was routed as a proxy request.')); + } + + $uri = new PhutilURI($uri); $username = AlmanacKeys::getClusterSSHUser(); if ($username === null) { @@ -148,17 +164,25 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { $repository = $this->identifyRepository(); $this->setRepository($repository); + // NOTE: Here, we're just figuring out if this is a proxyable request to + // a clusterized repository or not. We don't (and can't) use the URI we get + // back directly. + + // For example, we may get a read-only URI here but be handling a write + // request. We only care if we get back `null` (which means we should + // handle the request locally) or anything else (which means we should + // proxy it to an appropriate device). + $is_cluster_request = $this->getIsClusterRequest(); $uri = $repository->getAlmanacServiceURI( $viewer, - $is_cluster_request, array( - 'ssh', + 'neverProxy' => $is_cluster_request, + 'protocols' => array( + 'ssh', + ), )); - - if ($uri) { - $this->proxyURI = $uri; - } + $this->shouldProxy = (bool)$uri; try { return $this->executeRepositoryOperations(); diff --git a/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php index 0df4aa17fe..76dae05971 100644 --- a/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php @@ -148,7 +148,10 @@ final class DiffusionSubversionServeSSHWorkflow } if ($this->shouldProxy()) { - $command = $this->getProxyCommand(); + // NOTE: We're always requesting a writable device here. The request + // might be read-only, but we can't currently tell, and SVN requests + // can mix reads and writes. + $command = $this->getProxyCommand(true); $this->isProxying = true; $cwd = null; } else { diff --git a/src/applications/files/application/PhabricatorFilesApplication.php b/src/applications/files/application/PhabricatorFilesApplication.php index 5bf1ebccc4..14bbd83e78 100644 --- a/src/applications/files/application/PhabricatorFilesApplication.php +++ b/src/applications/files/application/PhabricatorFilesApplication.php @@ -73,7 +73,7 @@ final class PhabricatorFilesApplication extends PhabricatorApplication { => 'PhabricatorFileViewController', '/file/' => array( '(query/(?P[^/]+)/)?' => 'PhabricatorFileListController', - 'view/(?P[^/]+)/'. + 'view/(?P[1-9]\d*)/'. '(?:(?P[^/]+)/)?'. '(?:\$(?P\d+(?:-\d+)?))?' => 'PhabricatorFileViewController', diff --git a/src/applications/files/controller/PhabricatorFileDocumentController.php b/src/applications/files/controller/PhabricatorFileDocumentController.php index 15fadaa289..645b0e8e92 100644 --- a/src/applications/files/controller/PhabricatorFileDocumentController.php +++ b/src/applications/files/controller/PhabricatorFileDocumentController.php @@ -3,15 +3,15 @@ final class PhabricatorFileDocumentController extends PhabricatorFileController { - private $file; - private $engine; - private $ref; - public function shouldAllowPublic() { return true; } public function handleRequest(AphrontRequest $request) { + $engine = id(new PhabricatorFileDocumentRenderingEngine()) + ->setRequest($request) + ->setController($this); + $viewer = $request->getViewer(); $file_phid = $request->getURIData('phid'); @@ -21,107 +21,16 @@ final class PhabricatorFileDocumentController ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { - return $this->newErrorResponse( + return $engine->newErrorResponse( pht( 'This file ("%s") does not exist or could not be loaded.', $file_phid)); } - $this->file = $file; $ref = id(new PhabricatorDocumentRef()) ->setFile($file); - $this->ref = $ref; - $engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref); - $engine_key = $request->getURIData('engineKey'); - if (!isset($engines[$engine_key])) { - return $this->newErrorResponse( - pht( - 'The engine ("%s") is unknown, or unable to render this document.', - $engine_key)); - } - $engine = $engines[$engine_key]; - $this->engine = $engine; - - $encode_setting = $request->getStr('encode'); - if (strlen($encode_setting)) { - $engine->setEncodingConfiguration($encode_setting); - } - - $highlight_setting = $request->getStr('highlight'); - if (strlen($highlight_setting)) { - $engine->setHighlightingConfiguration($highlight_setting); - } - - try { - $content = $engine->newDocument($ref); - } catch (Exception $ex) { - return $this->newErrorResponse($ex->getMessage()); - } - - return $this->newContentResponse($content); - } - - private function newErrorResponse($message) { - $container = phutil_tag( - 'div', - array( - 'class' => 'document-engine-error', - ), - array( - id(new PHUIIconView()) - ->setIcon('fa-exclamation-triangle red'), - ' ', - $message, - )); - - return $this->newContentResponse($container); - } - - - private function newContentResponse($content) { - $viewer = $this->getViewer(); - $request = $this->getRequest(); - - $file = $this->file; - $engine = $this->engine; - $ref = $this->ref; - - if ($request->isAjax()) { - return id(new AphrontAjaxResponse()) - ->setContent( - array( - 'markup' => hsprintf('%s', $content), - )); - } - - $crumbs = $this->buildApplicationCrumbs(); - if ($file) { - $crumbs->addTextCrumb($file->getMonogram(), $file->getInfoURI()); - } - - $label = $engine->getViewAsLabel($ref); - if ($label) { - $crumbs->addTextCrumb($label); - } - - $crumbs->setBorder(true); - - $content_frame = id(new PHUIObjectBoxView()) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($content); - - $page_frame = id(new PHUITwoColumnView()) - ->setFooter($content_frame); - - return $this->newPage() - ->setCrumbs($crumbs) - ->setTitle( - array( - $ref->getName(), - pht('Standalone'), - )) - ->appendChild($page_frame); + return $engine->newRenderResponse($ref); } } diff --git a/src/applications/files/controller/PhabricatorFileViewController.php b/src/applications/files/controller/PhabricatorFileViewController.php index 9d32ff6fb1..42a632424d 100644 --- a/src/applications/files/controller/PhabricatorFileViewController.php +++ b/src/applications/files/controller/PhabricatorFileViewController.php @@ -403,122 +403,15 @@ final class PhabricatorFileViewController extends PhabricatorFileController { } private function newFileContent(PhabricatorFile $file) { - $viewer = $this->getViewer(); $request = $this->getRequest(); $ref = id(new PhabricatorDocumentRef()) ->setFile($file); - $engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref); + $engine = id(new PhabricatorFileDocumentRenderingEngine()) + ->setRequest($request); - $engine_key = $request->getURIData('engineKey'); - if (!isset($engines[$engine_key])) { - $engine_key = head_key($engines); - } - $engine = $engines[$engine_key]; - - $lines = $request->getURILineRange('lines', 1000); - if ($lines) { - $engine->setHighlightedLines(range($lines[0], $lines[1])); - } - - $encode_setting = $request->getStr('encode'); - if (strlen($encode_setting)) { - $engine->setEncodingConfiguration($encode_setting); - } - - $highlight_setting = $request->getStr('highlight'); - if (strlen($highlight_setting)) { - $engine->setHighlightingConfiguration($highlight_setting); - } - - $views = array(); - foreach ($engines as $candidate_key => $candidate_engine) { - $label = $candidate_engine->getViewAsLabel($ref); - if ($label === null) { - continue; - } - - $view_uri = '/file/view/'.$file->getID().'/'.$candidate_key.'/'; - - $view_icon = $candidate_engine->getViewAsIconIcon($ref); - $view_color = $candidate_engine->getViewAsIconColor($ref); - $loading = $candidate_engine->newLoadingContent($ref); - - $views[] = array( - 'viewKey' => $candidate_engine->getDocumentEngineKey(), - 'icon' => $view_icon, - 'color' => $view_color, - 'name' => $label, - 'engineURI' => $candidate_engine->getRenderURI($ref), - 'viewURI' => $view_uri, - 'loadingMarkup' => hsprintf('%s', $loading), - 'canEncode' => $candidate_engine->canConfigureEncoding($ref), - 'canHighlight' => $candidate_engine->CanConfigureHighlighting($ref), - ); - } - - $viewport_id = celerity_generate_unique_node_id(); - $control_id = celerity_generate_unique_node_id(); - $icon = $engine->newDocumentIcon($ref); - - if ($engine->shouldRenderAsync($ref)) { - $content = $engine->newLoadingContent($ref); - $config = array( - 'renderControlID' => $control_id, - ); - } else { - $content = $engine->newDocument($ref); - $config = array(); - } - - Javelin::initBehavior('document-engine', $config); - - $viewport = phutil_tag( - 'div', - array( - 'id' => $viewport_id, - ), - $content); - - $meta = array( - 'viewportID' => $viewport_id, - 'viewKey' => $engine->getDocumentEngineKey(), - 'views' => $views, - 'standaloneURI' => $engine->getRenderURI($ref), - 'encode' => array( - 'icon' => 'fa-font', - 'name' => pht('Change Text Encoding...'), - 'uri' => '/services/encoding/', - 'value' => $encode_setting, - ), - 'highlight' => array( - 'icon' => 'fa-lightbulb-o', - 'name' => pht('Highlight As...'), - 'uri' => '/services/highlight/', - 'value' => $highlight_setting, - ), - ); - - $view_button = id(new PHUIButtonView()) - ->setTag('a') - ->setText(pht('View Options')) - ->setIcon('fa-file-image-o') - ->setColor(PHUIButtonView::GREY) - ->setID($control_id) - ->setMetadata($meta) - ->setDropdown(true) - ->addSigil('document-engine-view-dropdown'); - - $header = id(new PHUIHeaderView()) - ->setHeaderIcon($icon) - ->setHeader($ref->getName()) - ->addActionLink($view_button); - - return id(new PHUIObjectBoxView()) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setHeader($header) - ->appendChild($viewport); + return $engine->newDocumentView($ref); } } diff --git a/src/applications/files/document/PhabricatorDocumentEngine.php b/src/applications/files/document/PhabricatorDocumentEngine.php index c3a1a7317a..85219b904c 100644 --- a/src/applications/files/document/PhabricatorDocumentEngine.php +++ b/src/applications/files/document/PhabricatorDocumentEngine.php @@ -38,6 +38,10 @@ abstract class PhabricatorDocumentEngine return false; } + public function canBlame(PhabricatorDocumentRef $ref) { + return false; + } + final public function setEncodingConfiguration($config) { $this->encodingConfiguration = $config; return $this; @@ -152,18 +156,6 @@ abstract class PhabricatorDocumentEngine return null; } - public function getRenderURI(PhabricatorDocumentRef $ref) { - $file = $ref->getFile(); - if (!$file) { - throw new PhutilMethodNotImplementedException(); - } - - $engine_key = $this->getDocumentEngineKey(); - $file_phid = $file->getPHID(); - - return "/file/document/{$engine_key}/{$file_phid}/"; - } - final public static function getEnginesForRef( PhabricatorUser $viewer, PhabricatorDocumentRef $ref) { diff --git a/src/applications/files/document/PhabricatorDocumentRef.php b/src/applications/files/document/PhabricatorDocumentRef.php index cca0c102e2..953ac7df5d 100644 --- a/src/applications/files/document/PhabricatorDocumentRef.php +++ b/src/applications/files/document/PhabricatorDocumentRef.php @@ -8,6 +8,8 @@ final class PhabricatorDocumentRef private $file; private $byteLength; private $snippet; + private $symbolMetadata = array(); + private $blameURI; public function setFile(PhabricatorFile $file) { $this->file = $file; @@ -131,4 +133,22 @@ final class PhabricatorDocumentRef return $this->snippet; } + public function setSymbolMetadata(array $metadata) { + $this->symbolMetadata = $metadata; + return $this; + } + + public function getSymbolMetadata() { + return $this->symbolMetadata; + } + + public function setBlameURI($blame_uri) { + $this->blameURI = $blame_uri; + return $this; + } + + public function getBlameURI() { + return $this->blameURI; + } + } diff --git a/src/applications/files/document/PhabricatorJSONDocumentEngine.php b/src/applications/files/document/PhabricatorJSONDocumentEngine.php index 331a7e6820..683f1746e6 100644 --- a/src/applications/files/document/PhabricatorJSONDocumentEngine.php +++ b/src/applications/files/document/PhabricatorJSONDocumentEngine.php @@ -52,7 +52,7 @@ final class PhabricatorJSONDocumentEngine return array( $message, - $this->newTextDocumentContent($content), + $this->newTextDocumentContent($ref, $content), ); } diff --git a/src/applications/files/document/PhabricatorSourceDocumentEngine.php b/src/applications/files/document/PhabricatorSourceDocumentEngine.php index cd7c2af92b..20a1c94a95 100644 --- a/src/applications/files/document/PhabricatorSourceDocumentEngine.php +++ b/src/applications/files/document/PhabricatorSourceDocumentEngine.php @@ -13,6 +13,10 @@ final class PhabricatorSourceDocumentEngine return true; } + public function canBlame(PhabricatorDocumentRef $ref) { + return true; + } + protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) { return 'fa-code'; } @@ -24,18 +28,39 @@ final class PhabricatorSourceDocumentEngine protected function newDocumentContent(PhabricatorDocumentRef $ref) { $content = $this->loadTextData($ref); + $messages = array(); + $highlighting = $this->getHighlightingConfiguration(); if ($highlighting !== null) { $content = PhabricatorSyntaxHighlighter::highlightWithLanguage( $highlighting, $content); } else { - $content = PhabricatorSyntaxHighlighter::highlightWithFilename( - $ref->getName(), - $content); + $highlight_limit = DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT; + if (strlen($content) > $highlight_limit) { + $messages[] = $this->newMessage( + pht( + 'This file is larger than %s, so syntax highlighting was skipped.', + phutil_format_bytes($highlight_limit))); + } else { + $content = PhabricatorSyntaxHighlighter::highlightWithFilename( + $ref->getName(), + $content); + } } - return $this->newTextDocumentContent($content); + $options = array(); + if ($ref->getBlameURI()) { + $content = phutil_split_lines($content); + $blame = range(1, count($content)); + $blame = array_fuse($blame); + $options['blame'] = $blame; + } + + return array( + $messages, + $this->newTextDocumentContent($ref, $content, $options), + ); } } diff --git a/src/applications/files/document/PhabricatorTextDocumentEngine.php b/src/applications/files/document/PhabricatorTextDocumentEngine.php index 08a5cabe54..0377a24353 100644 --- a/src/applications/files/document/PhabricatorTextDocumentEngine.php +++ b/src/applications/files/document/PhabricatorTextDocumentEngine.php @@ -13,12 +13,32 @@ abstract class PhabricatorTextDocumentEngine return true; } - protected function newTextDocumentContent($content) { - $lines = phutil_split_lines($content); + protected function newTextDocumentContent( + PhabricatorDocumentRef $ref, + $content, + array $options = array()) { + + PhutilTypeSpec::checkMap( + $options, + array( + 'blame' => 'optional wild', + )); + + if (is_array($content)) { + $lines = $content; + } else { + $lines = phutil_split_lines($content); + } $view = id(new PhabricatorSourceCodeView()) ->setHighlights($this->getHighlightedLines()) - ->setLines($lines); + ->setLines($lines) + ->setSymbolMetadata($ref->getSymbolMetadata()); + + $blame = idx($options, 'blame'); + if ($blame !== null) { + $view->setBlameMap($blame); + } $message = null; if ($this->encodingMessage !== null) { diff --git a/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php b/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php new file mode 100644 index 0000000000..4a16f53fdf --- /dev/null +++ b/src/applications/files/document/render/PhabricatorDocumentRenderingEngine.php @@ -0,0 +1,310 @@ +request = $request; + return $this; + } + + final public function getRequest() { + if (!$this->request) { + throw new PhutilInvalidStateException('setRequest'); + } + + return $this->request; + } + + final public function setController(PhabricatorController $controller) { + $this->controller = $controller; + return $this; + } + + final public function getController() { + if (!$this->controller) { + throw new PhutilInvalidStateException('setController'); + } + + return $this->controller; + } + + final protected function getActiveEngine() { + return $this->activeEngine; + } + + final protected function getRef() { + return $this->ref; + } + + final public function newDocumentView(PhabricatorDocumentRef $ref) { + $request = $this->getRequest(); + $viewer = $request->getViewer(); + + $engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref); + + $engine_key = $this->getSelectedDocumentEngineKey(); + if (!isset($engines[$engine_key])) { + $engine_key = head_key($engines); + } + $engine = $engines[$engine_key]; + + $lines = $this->getSelectedLineRange(); + if ($lines) { + $engine->setHighlightedLines(range($lines[0], $lines[1])); + } + + $encode_setting = $request->getStr('encode'); + if (strlen($encode_setting)) { + $engine->setEncodingConfiguration($encode_setting); + } + + $highlight_setting = $request->getStr('highlight'); + if (strlen($highlight_setting)) { + $engine->setHighlightingConfiguration($highlight_setting); + } + + $views = array(); + foreach ($engines as $candidate_key => $candidate_engine) { + $label = $candidate_engine->getViewAsLabel($ref); + if ($label === null) { + continue; + } + + $view_uri = $this->newRefViewURI($ref, $candidate_engine); + + $view_icon = $candidate_engine->getViewAsIconIcon($ref); + $view_color = $candidate_engine->getViewAsIconColor($ref); + $loading = $candidate_engine->newLoadingContent($ref); + + $views[] = array( + 'viewKey' => $candidate_engine->getDocumentEngineKey(), + 'icon' => $view_icon, + 'color' => $view_color, + 'name' => $label, + 'engineURI' => $this->newRefRenderURI($ref, $candidate_engine), + 'viewURI' => $view_uri, + 'loadingMarkup' => hsprintf('%s', $loading), + 'canEncode' => $candidate_engine->canConfigureEncoding($ref), + 'canHighlight' => $candidate_engine->canConfigureHighlighting($ref), + 'canBlame' => $candidate_engine->canBlame($ref), + ); + } + + $viewport_id = celerity_generate_unique_node_id(); + $control_id = celerity_generate_unique_node_id(); + $icon = $engine->newDocumentIcon($ref); + + $config = array( + 'controlID' => $control_id, + ); + + if ($engine->shouldRenderAsync($ref)) { + $content = $engine->newLoadingContent($ref); + $config['next'] = 'render'; + } else { + $this->willRenderRef($ref); + $content = $engine->newDocument($ref); + + if ($engine->canBlame($ref)) { + $config['next'] = 'blame'; + } + } + + Javelin::initBehavior('document-engine', $config); + + $viewport = phutil_tag( + 'div', + array( + 'id' => $viewport_id, + ), + $content); + + $meta = array( + 'viewportID' => $viewport_id, + 'viewKey' => $engine->getDocumentEngineKey(), + 'views' => $views, + 'encode' => array( + 'icon' => 'fa-font', + 'name' => pht('Change Text Encoding...'), + 'uri' => '/services/encoding/', + 'value' => $encode_setting, + ), + 'highlight' => array( + 'icon' => 'fa-lightbulb-o', + 'name' => pht('Highlight As...'), + 'uri' => '/services/highlight/', + 'value' => $highlight_setting, + ), + 'blame' => array( + 'uri' => $ref->getBlameURI(), + 'value' => null, + ), + ); + + $view_button = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('View Options')) + ->setIcon('fa-file-image-o') + ->setColor(PHUIButtonView::GREY) + ->setID($control_id) + ->setMetadata($meta) + ->setDropdown(true) + ->addSigil('document-engine-view-dropdown'); + + $header = id(new PHUIHeaderView()) + ->setHeaderIcon($icon) + ->setHeader($ref->getName()) + ->addActionLink($view_button); + + return id(new PHUIObjectBoxView()) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setHeader($header) + ->appendChild($viewport); + } + + final public function newRenderResponse(PhabricatorDocumentRef $ref) { + $this->willRenderRef($ref); + + $request = $this->getRequest(); + $viewer = $request->getViewer(); + + $engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref); + $engine_key = $this->getSelectedDocumentEngineKey(); + if (!isset($engines[$engine_key])) { + return $this->newErrorResponse( + pht( + 'The engine ("%s") is unknown, or unable to render this document.', + $engine_key)); + } + $engine = $engines[$engine_key]; + + $this->activeEngine = $engine; + + $encode_setting = $request->getStr('encode'); + if (strlen($encode_setting)) { + $engine->setEncodingConfiguration($encode_setting); + } + + $highlight_setting = $request->getStr('highlight'); + if (strlen($highlight_setting)) { + $engine->setHighlightingConfiguration($highlight_setting); + } + + try { + $content = $engine->newDocument($ref); + } catch (Exception $ex) { + return $this->newErrorResponse($ex->getMessage()); + } + + return $this->newContentResponse($content); + } + + public function newErrorResponse($message) { + $container = phutil_tag( + 'div', + array( + 'class' => 'document-engine-error', + ), + array( + id(new PHUIIconView()) + ->setIcon('fa-exclamation-triangle red'), + ' ', + $message, + )); + + return $this->newContentResponse($container); + } + + private function newContentResponse($content) { + $request = $this->getRequest(); + $viewer = $request->getViewer(); + $controller = $this->getController(); + + if ($request->isAjax()) { + return id(new AphrontAjaxResponse()) + ->setContent( + array( + 'markup' => hsprintf('%s', $content), + )); + } + + $crumbs = $this->newCrumbs(); + $crumbs->setBorder(true); + + $content_frame = id(new PHUIObjectBoxView()) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($content); + + $page_frame = id(new PHUITwoColumnView()) + ->setFooter($content_frame); + + $title = array(); + $ref = $this->getRef(); + if ($ref) { + $title = array( + $ref->getName(), + pht('Standalone'), + ); + } else { + $title = pht('Document'); + } + + return $controller->newPage() + ->setCrumbs($crumbs) + ->setTitle($title) + ->appendChild($page_frame); + } + + protected function newCrumbs() { + $engine = $this->getActiveEngine(); + $controller = $this->getController(); + + $crumbs = $controller->buildApplicationCrumbsForEditEngine(); + + $ref = $this->getRef(); + + $this->addApplicationCrumbs($crumbs, $ref); + + if ($ref) { + $label = $engine->getViewAsLabel($ref); + if ($label) { + $crumbs->addTextCrumb($label); + } + } + + return $crumbs; + } + + abstract protected function newRefViewURI( + PhabricatorDocumentRef $ref, + PhabricatorDocumentEngine $engine); + + abstract protected function newRefRenderURI( + PhabricatorDocumentRef $ref, + PhabricatorDocumentEngine $engine); + + protected function getSelectedDocumentEngineKey() { + return $this->getRequest()->getURIData('engineKey'); + } + + protected function getSelectedLineRange() { + return $this->getRequest()->getURILineRange('lines', 1000); + } + + protected function addApplicationCrumbs( + PHUICrumbsView $crumbs, + PhabricatorDocumentRef $ref = null) { + return; + } + + protected function willRenderRef(PhabricatorDocumentRef $ref) { + return; + } + +} diff --git a/src/applications/files/document/render/PhabricatorFileDocumentRenderingEngine.php b/src/applications/files/document/render/PhabricatorFileDocumentRenderingEngine.php new file mode 100644 index 0000000000..65e9a49681 --- /dev/null +++ b/src/applications/files/document/render/PhabricatorFileDocumentRenderingEngine.php @@ -0,0 +1,49 @@ +getFile(); + $engine_key = $engine->getDocumentEngineKey(); + + return urisprintf( + '/file/view/%d/%s/', + $file->getID(), + $engine_key); + } + + protected function newRefRenderURI( + PhabricatorDocumentRef $ref, + PhabricatorDocumentEngine $engine) { + $file = $ref->getFile(); + if (!$file) { + throw new PhutilMethodNotImplementedException(); + } + + $engine_key = $engine->getDocumentEngineKey(); + $file_phid = $file->getPHID(); + + return urisprintf( + '/file/document/%s/%s/', + $engine_key, + $file_phid); + } + + protected function addApplicationCrumbs( + PHUICrumbsView $crumbs, + PhabricatorDocumentRef $ref = null) { + + if ($ref) { + $file = $ref->getFile(); + if ($file) { + $crumbs->addTextCrumb($file->getMonogram(), $file->getInfoURI()); + } + } + + } + +} diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index a8d16b7651..b795678cd2 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -648,10 +648,17 @@ final class PhabricatorFile extends PhabricatorFileDAO // just bail out. throw $status; } else { - // This is HTTP 2XX, so use the response body to save the - // file data. + // This is HTTP 2XX, so use the response body to save the file data. + // Provide a default name based on the URI, truncating it if the URI + // is exceptionally long. + + $default_name = basename($uri); + $default_name = id(new PhutilUTF8StringTruncator()) + ->setMaximumBytes(64) + ->truncateString($default_name); + $params = $params + array( - 'name' => basename($uri), + 'name' => $default_name, ); return self::newFromFileData($body, $params); diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php index 0646bd9309..ac3a7101fd 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -77,7 +77,6 @@ final class HarbormasterBuildViewController pht('View Current Build'))); } - $curtain = $this->buildCurtainView($build); $properties = $this->buildPropertyList($build); $history = $this->buildHistoryTable( @@ -292,7 +291,7 @@ final class HarbormasterBuildViewController $targets[] = $target_box; - $targets[] = $this->buildLog($build, $build_target); + $targets[] = $this->buildLog($build, $build_target, $generation); } $timeline = $this->buildTransactionTimeline( @@ -371,7 +370,8 @@ final class HarbormasterBuildViewController private function buildLog( HarbormasterBuild $build, - HarbormasterBuildTarget $build_target) { + HarbormasterBuildTarget $build_target, + $generation) { $request = $this->getRequest(); $viewer = $request->getUser(); @@ -410,6 +410,8 @@ final class HarbormasterBuildViewController $log_view->setLines($lines); $log_view->setStart($start); + $subheader = $this->createLogHeader($build, $log, $limit, $generation); + $prototype_view = id(new PHUIButtonView()) ->setTag('a') ->setHref($log->getURI()) @@ -423,7 +425,7 @@ final class HarbormasterBuildViewController $log->getLogSource(), $log->getLogType())) ->addActionLink($prototype_view) - ->setSubheader($this->createLogHeader($build, $log)) + ->setSubheader($subheader) ->setUser($viewer); $log_box = id(new PHUIObjectBoxView()) @@ -479,43 +481,53 @@ final class HarbormasterBuildViewController return $log_boxes; } - private function createLogHeader($build, $log) { - $request = $this->getRequest(); - $limit = $request->getInt('l', 25); + private function createLogHeader($build, $log, $limit, $generation) { + $options = array( + array( + 'n' => 25, + ), + array( + 'n' => 50, + ), + array( + 'n' => 100, + ), + array( + 'n' => 0, + 'label' => pht('Unlimited'), + ), + ); - $lines_25 = $this->getApplicationURI('/build/'.$build->getID().'/?l=25'); - $lines_50 = $this->getApplicationURI('/build/'.$build->getID().'/?l=50'); - $lines_100 = - $this->getApplicationURI('/build/'.$build->getID().'/?l=100'); - $lines_0 = $this->getApplicationURI('/build/'.$build->getID().'/?l=0'); + $base_uri = id(new PhutilURI($build->getURI().$generation.'/')); - $link_25 = phutil_tag('a', array('href' => $lines_25), pht('25')); - $link_50 = phutil_tag('a', array('href' => $lines_50), pht('50')); - $link_100 = phutil_tag('a', array('href' => $lines_100), pht('100')); - $link_0 = phutil_tag('a', array('href' => $lines_0), pht('Unlimited')); + $links = array(); + foreach ($options as $option) { + $n = $option['n']; + $label = idx($option, 'label', $n); - if ($limit === 25) { - $link_25 = phutil_tag('strong', array(), $link_25); - } else if ($limit === 50) { - $link_50 = phutil_tag('strong', array(), $link_50); - } else if ($limit === 100) { - $link_100 = phutil_tag('strong', array(), $link_100); - } else if ($limit === 0) { - $link_0 = phutil_tag('strong', array(), $link_0); + $is_selected = ($limit == $n); + if ($is_selected) { + $links[] = phutil_tag( + 'strong', + array(), + $label); + } else { + $links[] = phutil_tag( + 'a', + array( + 'href' => (string)$base_uri->alter('l', $n), + ), + $label); + } } return phutil_tag( 'span', array(), array( - $link_25, - ' - ', - $link_50, - ' - ', - $link_100, - ' - ', - $link_0, - ' Lines', + phutil_implode_html(' - ', $links), + ' ', + pht('Lines'), )); } diff --git a/src/applications/harbormaster/view/ShellLogView.php b/src/applications/harbormaster/view/ShellLogView.php index a78840741c..c61237db10 100644 --- a/src/applications/harbormaster/view/ShellLogView.php +++ b/src/applications/harbormaster/view/ShellLogView.php @@ -65,7 +65,6 @@ final class ShellLogView extends AphrontView { 'th', array( 'class' => 'phabricator-source-line', - 'style' => 'background-color: #fff;', ), $content_number); @@ -95,13 +94,11 @@ final class ShellLogView extends AphrontView { 'div', array( 'class' => 'phabricator-source-code-container', - 'style' => 'background-color: black; color: white;', ), phutil_tag( 'table', array( 'class' => implode(' ', $classes), - 'style' => 'background-color: black', ), phutil_implode_html('', $rows))); } diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php index 0075770863..e57b9b545e 100644 --- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php +++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php @@ -6,10 +6,6 @@ final class PhabricatorManiphestApplication extends PhabricatorApplication { return pht('Maniphest'); } - public function getMenuName() { - return pht('Tasks'); - } - public function getShortDescription() { return pht('Tasks and Bugs'); } diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index f7c1551be7..edb780b724 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -26,6 +26,7 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $closedEpochMin; private $closedEpochMax; private $closerPHIDs; + private $columnPHIDs; private $status = 'status-any'; const STATUS_ANY = 'status-any'; @@ -213,6 +214,11 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { return $this; } + public function withColumnPHIDs(array $column_phids) { + $this->columnPHIDs = $column_phids; + return $this; + } + public function newResultObject() { return new ManiphestTask(); } @@ -442,6 +448,91 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { $this->subtypes); } + + if ($this->columnPHIDs !== null) { + $viewer = $this->getViewer(); + + $columns = id(new PhabricatorProjectColumnQuery()) + ->setParentQuery($this) + ->setViewer($viewer) + ->withPHIDs($this->columnPHIDs) + ->execute(); + if (!$columns) { + throw new PhabricatorEmptyQueryException(); + } + + // We must do board layout before we move forward because the column + // positions may not yet exist otherwise. An example is that newly + // created tasks may not yet be positioned in the backlog column. + + $projects = mpull($columns, 'getProject'); + $projects = mpull($projects, null, 'getPHID'); + + // The board layout engine needs to know about every object that it's + // going to be asked to do layout for. For now, we're just doing layout + // on every object on the boards. In the future, we could do layout on a + // smaller set of objects by using the constraints on this Query. For + // example, if the caller is only asking for open tasks, we only need + // to do layout on open tasks. + + // This fetches too many objects (every type of object tagged with the + // project, not just tasks). We could narrow it by querying the edge + // table on the Maniphest side, but there's currently no way to build + // that query with EdgeQuery. + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array_keys($projects)) + ->withEdgeTypes( + array( + PhabricatorProjectProjectHasObjectEdgeType::EDGECONST, + )); + + $edge_query->execute(); + $all_phids = $edge_query->getDestinationPHIDs(); + + // Since we overfetched PHIDs, filter out any non-tasks we got back. + foreach ($all_phids as $key => $phid) { + if (phid_get_type($phid) !== ManiphestTaskPHIDType::TYPECONST) { + unset($all_phids[$key]); + } + } + + // If there are no tasks on the relevant boards, this query can't + // possibly hit anything so we're all done. + $task_phids = array_fuse($all_phids); + if (!$task_phids) { + throw new PhabricatorEmptyQueryException(); + } + + // We know everything we need to know, so perform board layout. + $engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setFetchAllBoards(true) + ->setBoardPHIDs(array_keys($projects)) + ->setObjectPHIDs($task_phids) + ->executeLayout(); + + // Find the tasks that are in the constraint columns after board layout + // completes. + $select_phids = array(); + foreach ($columns as $column) { + $in_column = $engine->getColumnObjectPHIDs( + $column->getProjectPHID(), + $column->getPHID()); + foreach ($in_column as $phid) { + $select_phids[$phid] = $phid; + } + } + + if (!$select_phids) { + throw new PhabricatorEmptyQueryException(); + } + + $where[] = qsprintf( + $conn, + 'task.phid IN (%Ls)', + $select_phids); + } + return $where; } diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php index 565bc7a8f4..f85c79621c 100644 --- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php +++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php @@ -86,6 +86,10 @@ final class ManiphestTaskSearchEngine pht('Search for tasks with given subtypes.')) ->setDatasource(new ManiphestTaskSubtypeDatasource()) ->setIsHidden($hide_subtypes), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Columns')) + ->setKey('columnPHIDs') + ->setAliases(array('column', 'columnPHID', 'columns')), id(new PhabricatorSearchThreeStateField()) ->setLabel(pht('Open Parents')) ->setKey('hasParents') @@ -246,6 +250,10 @@ final class ManiphestTaskSearchEngine $query->withSubtaskIDs($map['subtaskIDs']); } + if ($map['columnPHIDs']) { + $query->withColumnPHIDs($map['columnPHIDs']); + } + $group = idx($map, 'group'); $group = idx($this->getGroupValues(), $group); if ($group) { diff --git a/src/applications/meta/controller/PhabricatorApplicationUninstallController.php b/src/applications/meta/controller/PhabricatorApplicationUninstallController.php index 68518413bf..09f3e92ee5 100644 --- a/src/applications/meta/controller/PhabricatorApplicationUninstallController.php +++ b/src/applications/meta/controller/PhabricatorApplicationUninstallController.php @@ -3,17 +3,15 @@ final class PhabricatorApplicationUninstallController extends PhabricatorApplicationsController { - private $application; - private $action; - public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); - $this->action = $request->getURIData('action'); - $this->application = $request->getURIData('application'); + $user = $request->getUser(); + $action = $request->getURIData('action'); + $application_name = $request->getURIData('application'); - $selected = id(new PhabricatorApplicationQuery()) + $application = id(new PhabricatorApplicationQuery()) ->setViewer($viewer) - ->withClasses(array($this->application)) + ->withClasses(array($application_name)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, @@ -21,11 +19,11 @@ final class PhabricatorApplicationUninstallController )) ->executeOne(); - if (!$selected) { + if (!$application) { return new Aphront404Response(); } - $view_uri = $this->getApplicationURI('view/'.$this->application); + $view_uri = $this->getApplicationURI('view/'.$application_name); $prototypes_enabled = PhabricatorEnv::getEnvConfig( 'phabricator.show-prototypes'); @@ -34,7 +32,7 @@ final class PhabricatorApplicationUninstallController ->setUser($viewer) ->addCancelButton($view_uri); - if ($selected->isPrototype() && !$prototypes_enabled) { + if ($application->isPrototype() && !$prototypes_enabled) { $dialog ->setTitle(pht('Prototypes Not Enabled')) ->appendChild( @@ -46,18 +44,40 @@ final class PhabricatorApplicationUninstallController } if ($request->isDialogFormPost()) { - $this->manageApplication(); - return id(new AphrontRedirectResponse())->setURI($view_uri); + $xactions = array(); + $template = $application->getApplicationTransactionTemplate(); + $xactions[] = id(clone $template) + ->setTransactionType( + PhabricatorApplicationUninstallTransaction::TRANSACTIONTYPE) + ->setNewValue($action); + + $editor = id(new PhabricatorApplicationEditor()) + ->setActor($user) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + try { + $editor->applyTransactions($application, $xactions); + return id(new AphrontRedirectResponse())->setURI($view_uri); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + } + + return $this->newDialog() + ->setTitle(pht('Validation Failed')) + ->setValidationException($validation_exception) + ->addCancelButton($view_uri); } - if ($this->action == 'install') { - if ($selected->canUninstall()) { + if ($action == 'install') { + if ($application->canUninstall()) { $dialog ->setTitle(pht('Confirmation')) ->appendChild( pht( 'Install %s application?', - $selected->getName())) + $application->getName())) ->addSubmitButton(pht('Install')); } else { @@ -66,10 +86,10 @@ final class PhabricatorApplicationUninstallController ->appendChild(pht('You cannot install an installed application.')); } } else { - if ($selected->canUninstall()) { + if ($application->canUninstall()) { $dialog->setTitle(pht('Really Uninstall Application?')); - if ($selected instanceof PhabricatorHomeApplication) { + if ($application instanceof PhabricatorHomeApplication) { $dialog ->appendParagraph( pht( @@ -86,7 +106,7 @@ final class PhabricatorApplicationUninstallController ->appendParagraph( pht( 'Really uninstall the %s application?', - $selected->getName())) + $application->getName())) ->addSubmitButton(pht('Uninstall')); } } else { @@ -101,23 +121,4 @@ final class PhabricatorApplicationUninstallController return id(new AphrontDialogResponse())->setDialog($dialog); } - public function manageApplication() { - $key = 'phabricator.uninstalled-applications'; - $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); - $list = $config_entry->getValue(); - $uninstalled = PhabricatorEnv::getEnvConfig($key); - - if (isset($uninstalled[$this->application])) { - unset($list[$this->application]); - } else { - $list[$this->application] = true; - } - - PhabricatorConfigEditor::storeNewValue( - $this->getViewer(), - $config_entry, - $list, - PhabricatorContentSource::newFromRequest($this->getRequest())); - } - } diff --git a/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php b/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php index dca2f5a795..91b5c73249 100644 --- a/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php +++ b/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php @@ -14,7 +14,7 @@ final class PhabricatorApplicationPolicyChangeTransaction return $application->getPolicy($capability); } - public function applyInternalEffects($object, $value) { + public function applyExternalEffects($object, $value) { $application = $object; $user = $this->getActor(); diff --git a/src/applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php b/src/applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php new file mode 100644 index 0000000000..12fbc8ebd4 --- /dev/null +++ b/src/applications/meta/xactions/PhabricatorApplicationUninstallTransaction.php @@ -0,0 +1,79 @@ +getValue(); + $uninstalled = PhabricatorEnv::getEnvConfig($key); + + if (isset($uninstalled[get_class($object)])) { + return 'uninstalled'; + } else { + return 'installed'; + } + } + + public function generateNewValue($object, $value) { + if ($value === 'uninstall') { + return 'uninstalled'; + } else { + return 'installed'; + } + } + + public function applyExternalEffects($object, $value) { + $application = $object; + $user = $this->getActor(); + + $key = 'phabricator.uninstalled-applications'; + $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); + $list = $config_entry->getValue(); + $uninstalled = PhabricatorEnv::getEnvConfig($key); + + if (isset($uninstalled[get_class($application)])) { + unset($list[get_class($application)]); + } else { + $list[get_class($application)] = true; + } + + $editor = $this->getEditor(); + $content_source = $editor->getContentSource(); + PhabricatorConfigEditor::storeNewValue( + $user, + $config_entry, + $list, + $content_source); + } + + public function getTitle() { + if ($this->getNewValue() === 'uninstalled') { + return pht( + '%s uninstalled this application.', + $this->renderAuthor()); + } else { + return pht( + '%s installed this application.', + $this->renderAuthor()); + } + } + + public function getTitleForFeed() { + if ($this->getNewValue() === 'uninstalled') { + return pht( + '%s uninstalled %s.', + $this->renderAuthor(), + $this->renderObject()); + } else { + return pht( + '%s installed %s.', + $this->renderAuthor(), + $this->renderObject()); + } + } + +} diff --git a/src/applications/phame/application/PhabricatorPhameApplication.php b/src/applications/phame/application/PhabricatorPhameApplication.php index f9ee2831b2..9342076ddd 100644 --- a/src/applications/phame/application/PhabricatorPhameApplication.php +++ b/src/applications/phame/application/PhabricatorPhameApplication.php @@ -6,10 +6,6 @@ final class PhabricatorPhameApplication extends PhabricatorApplication { return pht('Phame'); } - public function getMenuName() { - return pht('Blogs'); - } - public function getBaseURI() { return '/phame/'; } diff --git a/src/applications/pholio/application/PhabricatorPholioApplication.php b/src/applications/pholio/application/PhabricatorPholioApplication.php index 4afaeba263..4d80dd12ae 100644 --- a/src/applications/pholio/application/PhabricatorPholioApplication.php +++ b/src/applications/pholio/application/PhabricatorPholioApplication.php @@ -6,10 +6,6 @@ final class PhabricatorPholioApplication extends PhabricatorApplication { return pht('Pholio'); } - public function getMenuName() { - return pht('Design Review'); - } - public function getBaseURI() { return '/pholio/'; } diff --git a/src/applications/phriction/application/PhabricatorPhrictionApplication.php b/src/applications/phriction/application/PhabricatorPhrictionApplication.php index 1b06fd197c..ea91727c76 100644 --- a/src/applications/phriction/application/PhabricatorPhrictionApplication.php +++ b/src/applications/phriction/application/PhabricatorPhrictionApplication.php @@ -6,10 +6,6 @@ final class PhabricatorPhrictionApplication extends PhabricatorApplication { return pht('Phriction'); } - public function getMenuName() { - return pht('Wiki'); - } - public function getShortDescription() { return pht('Wiki Documents'); } diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 9396d1873e..bb048f22ea 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -36,7 +36,8 @@ final class PhabricatorProjectBoardViewController if ($request->isFormPost() && !$request->getBool('initialize') - && !$request->getStr('move')) { + && !$request->getStr('move') + && !$request->getStr('queryColumnID')) { $saved = $search_engine->buildSavedQueryFromRequest($request); $search_engine->saveQuery($saved); $filter_form = id(new AphrontFormView()) @@ -188,6 +189,46 @@ final class PhabricatorProjectBoardViewController ->appendChild($content); } + // If the user wants to turn a particular column into a query, build an + // apropriate filter and redirect them to the query results page. + $query_column_id = $request->getInt('queryColumnID'); + if ($query_column_id) { + $column_id_map = mpull($columns, null, 'getID'); + $query_column = idx($column_id_map, $query_column_id); + if (!$query_column) { + return new Aphront404Response(); + } + + // Create a saved query to combine the active filter on the workboard + // with the column filter. If the user currently has constraints on the + // board, we want to add a new column or project constraint, not + // completely replace the constraints. + $saved_query = clone $saved; + + if ($query_column->getProxyPHID()) { + $project_phids = $saved_query->getParameter('projectPHIDs'); + if (!$project_phids) { + $project_phids = array(); + } + $project_phids[] = $query_column->getProxyPHID(); + $saved_query->setParameter('projectPHIDs', $project_phids); + } else { + $saved_query->setParameter( + 'columnPHIDs', + array($query_column->getPHID())); + } + + $search_engine = id(new ManiphestTaskSearchEngine()) + ->setViewer($viewer); + $search_engine->saveQuery($saved_query); + + $query_key = $saved_query->getQueryKey(); + $query_uri = new PhutilURI("/maniphest/query/{$query_key}/#R"); + + return id(new AphrontRedirectResponse()) + ->setURI($query_uri); + } + $task_can_edit_map = id(new PhabricatorPolicyFilter()) ->setViewer($viewer) ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) @@ -1069,8 +1110,14 @@ final class PhabricatorProjectBoardViewController ->setHref($batch_move_uri) ->setWorkflow(true); - // Column Related Actions Below - // + $query_uri = $request->getRequestURI(); + $query_uri->setQueryParam('queryColumnID', $column->getID()); + + $column_items[] = id(new PhabricatorActionView()) + ->setName(pht('View as Query')) + ->setIcon('fa-search') + ->setHref($query_uri); + $edit_uri = 'board/'.$this->id.'/edit/'.$column->getID().'/'; $column_items[] = id(new PhabricatorActionView()) ->setName(pht('Edit Column')) diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 435e5749e6..0a09503ee9 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -703,7 +703,9 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO case 'history': case 'graph': case 'clone': + case 'blame': case 'browse': + case 'document': case 'change': case 'lastmodified': case 'tags': @@ -781,7 +783,9 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO case 'change': case 'history': case 'graph': + case 'blame': case 'browse': + case 'document': case 'lastmodified': case 'tags': case 'branches': @@ -1893,14 +1897,24 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO * services, returning raw URIs. * * @param PhabricatorUser Viewing user. - * @param bool `true` to throw if a remote URI would be returned. - * @param list List of allowable protocols. + * @param map Constraints on selectable services. * @return string|null URI, or `null` for local repositories. */ public function getAlmanacServiceURI( PhabricatorUser $viewer, - $never_proxy, - array $protocols) { + array $options) { + + PhutilTypeSpec::checkMap( + $options, + array( + 'neverProxy' => 'bool', + 'protocols' => 'list', + 'writable' => 'optional bool', + )); + + $never_proxy = $options['neverProxy']; + $protocols = $options['protocols']; + $writable = idx($options, 'writable', false); $cache_key = $this->getAlmanacServiceCacheKey(); if (!$cache_key) { @@ -1946,7 +1960,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } if (isset($protocol_map[$uri['protocol']])) { - $results[] = new PhutilURI($uri['uri']); + $results[] = $uri; } } @@ -1977,8 +1991,29 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } } + // If we require a writable device, remove URIs which aren't writable. + if ($writable) { + foreach ($results as $key => $uri) { + if (!$uri['writable']) { + unset($results[$key]); + } + } + + if (!$results) { + throw new Exception( + pht( + 'This repository ("%s") is not writable with the given '. + 'protocols (%s). The Almanac service for this repository has no '. + 'writable bindings that support these protocols.', + $this->getDisplayName(), + implode(', ', $protocols))); + } + } + shuffle($results); - return head($results); + + $result = head($results); + return $result['uri']; } public function supportsSynchronization() { @@ -1997,7 +2032,14 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } $repository_phid = $this->getPHID(); - return "diffusion.repository({$repository_phid}).service({$service_phid})"; + + $parts = array( + "repo({$repository_phid})", + "serv({$service_phid})", + 'v2', + ); + + return implode('.', $parts); } private function buildAlmanacServiceURIs() { @@ -2026,6 +2068,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO 'protocol' => $protocol, 'uri' => (string)$uri, 'device' => $device_name, + 'writable' => (bool)$binding->getAlmanacPropertyValue('writable'), ); } @@ -2073,10 +2116,16 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO $uri = $this->getAlmanacServiceURI( $viewer, - $never_proxy, array( - 'http', - 'https', + 'neverProxy' => $never_proxy, + 'protocols' => array( + 'http', + 'https', + ), + + // At least today, no Conduit call can ever write to a repository, + // so it's fine to send anything to a read-only node. + 'writable' => false, )); if ($uri === null) { return null; diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index f6e6e22c3a..01088522e7 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -416,28 +416,6 @@ final class PhabricatorRepositoryCommit return $repository->formatCommitName($identifier, $local = true); } - public function renderAuthorLink($handles) { - $author_phid = $this->getAuthorPHID(); - if ($author_phid && isset($handles[$author_phid])) { - return $handles[$author_phid]->renderLink(); - } - - return $this->renderAuthorShortName($handles); - } - - public function renderAuthorShortName($handles) { - $author_phid = $this->getAuthorPHID(); - if ($author_phid && isset($handles[$author_phid])) { - return $handles[$author_phid]->getName(); - } - - $data = $this->getCommitData(); - $name = $data->getAuthorName(); - - $parsed = new PhutilEmailAddress($name); - return nonempty($parsed->getDisplayName(), $parsed->getAddress()); - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php index 40275a8d7a..aa42d56cfb 100644 --- a/src/applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php @@ -31,7 +31,7 @@ final class PhabricatorApplicationProfileMenuItem return $name; } - return $application->getMenuName(); + return $application->getName(); } public function buildEditEngineFields( diff --git a/src/applications/transactions/editfield/PhabricatorIntEditField.php b/src/applications/transactions/editfield/PhabricatorIntEditField.php new file mode 100644 index 0000000000..690b4c0b17 --- /dev/null +++ b/src/applications/transactions/editfield/PhabricatorIntEditField.php @@ -0,0 +1,14 @@ + $source) { + // See T13119. Exclude proxy datasources from the dropdown since they + // fatal if built like this without actually being configured with an + // underlying datasource. This is a bit hacky but this is just a + // debugging/development UI anyway. + if ($source instanceof PhabricatorTypeaheadProxyDatasource) { + unset($sources[$key]); + continue; + } + // This can happen with composite or generic sources. if (!$source->getDatasourceApplicationClass()) { continue; } + if (!PhabricatorApplication::isClassInstalledForViewer( $source->getDatasourceApplicationClass(), $viewer)) { diff --git a/src/docs/user/cluster/cluster_repositories.diviner b/src/docs/user/cluster/cluster_repositories.diviner index 44003679af..8d764c2e3e 100644 --- a/src/docs/user/cluster/cluster_repositories.diviner +++ b/src/docs/user/cluster/cluster_repositories.diviner @@ -221,13 +221,33 @@ synchronized. Contracting a Cluster ===================== -To reduce the size of an existing cluster, follow these general steps: +If you want to remove working devices from a cluster (for example, to take +hosts down for maintenance), first do this for each device: - - Disable the bindings from the service to the dead device in Almanac. + - Change the `writable` property on the bindings to "Prevent Writes". + - Wait a few moments until the cluster synchronizes (see + "Monitoring Services" below). + +This will ensure that the device you're about to remove is not the only cluster +leader, even if the cluster is receiving a high write volume. You can skip this +step if the device isn't working property to start with. + +Once you've stopped writes and waited for synchronization (or if the hosts are +not working in the first place) do this for each device: + + - Disable the bindings from the service to the device in Almanac. If you are removing a device because it failed abruptly (or removing several -devices at once) it is possible that some repositories will have lost all their -leaders. See "Loss of Leaders" below to understand and resolve this. +devices at once; or you skip the "Prevent Writes" step), it is possible that +some repositories will have lost all their leaders. See "Loss of Leaders" below +to understand and resolve this. + +If you want to put the hosts back in service later: + + - Enable the bindings again. + - Change `writable` back to "Allow Writes". + +This will restore the cluster to the original state. Monitoring Services diff --git a/src/view/layout/PhabricatorSourceCodeView.php b/src/view/layout/PhabricatorSourceCodeView.php index ce26fa3df7..19126d1c6a 100644 --- a/src/view/layout/PhabricatorSourceCodeView.php +++ b/src/view/layout/PhabricatorSourceCodeView.php @@ -8,6 +8,8 @@ final class PhabricatorSourceCodeView extends AphrontView { private $canClickHighlight = true; private $truncatedFirstBytes = false; private $truncatedFirstLines = false; + private $symbolMetadata; + private $blameMap; public function setLines(array $lines) { $this->lines = $lines; @@ -39,11 +41,31 @@ final class PhabricatorSourceCodeView extends AphrontView { return $this; } + public function setSymbolMetadata(array $symbol_metadata) { + $this->symbolMetadata = $symbol_metadata; + return $this; + } + + public function getSymbolMetadata() { + return $this->symbolMetadata; + } + + public function setBlameMap(array $map) { + $this->blameMap = $map; + return $this; + } + + public function getBlameMap() { + return $this->blameMap; + } + public function render() { + $blame_map = $this->getBlameMap(); + $has_blame = ($blame_map !== null); + require_celerity_resource('phabricator-source-code-view-css'); require_celerity_resource('syntax-highlighting-css'); - Javelin::initBehavior('phabricator-oncopy', array()); if ($this->canClickHighlight) { Javelin::initBehavior('phabricator-line-linker'); } @@ -55,11 +77,11 @@ final class PhabricatorSourceCodeView extends AphrontView { $lines = $this->lines; if ($this->truncatedFirstLines) { $lines[] = phutil_tag( - 'span', - array( - 'class' => 'c', - ), - pht('...')); + 'span', + array( + 'class' => 'c', + ), + pht('...')); } else if ($this->truncatedFirstBytes) { $last_key = last_key($lines); $lines[$last_key] = hsprintf( @@ -74,14 +96,15 @@ final class PhabricatorSourceCodeView extends AphrontView { } $base_uri = (string)$this->uri; + $wrote_anchor = false; foreach ($lines as $line) { - - // NOTE: See phabricator-oncopy behavior. - $content_line = hsprintf("\xE2\x80\x8B%s", $line); - $row_attributes = array(); if (isset($this->highlights[$line_number])) { $row_attributes['class'] = 'phabricator-source-highlight'; + if (!$wrote_anchor) { + $row_attributes['id'] = 'phabricator-line-linker-anchor'; + $wrote_anchor = true; + } } if ($this->canClickHighlight) { @@ -95,8 +118,8 @@ final class PhabricatorSourceCodeView extends AphrontView { 'a', array( 'href' => $line_href, - ), - $line_number); + 'data-n' => $line_number, + )); } else { $tag_number = phutil_tag( 'span', @@ -104,10 +127,41 @@ final class PhabricatorSourceCodeView extends AphrontView { $line_number); } + if ($has_blame) { + $lines = idx($blame_map, $line_number); + + if ($lines) { + $skip_blame = 'skip'; + $info_blame = 'info'; + } else { + $skip_blame = null; + $info_blame = null; + } + + $blame_cells = array( + phutil_tag( + 'th', + array( + 'class' => 'phabricator-source-blame-skip', + 'data-blame' => $skip_blame, + )), + phutil_tag( + 'th', + array( + 'class' => 'phabricator-source-blame-info', + 'data-blame' => $info_blame, + 'data-blame-lines' => $lines, + )), + ); + } else { + $blame_cells = null; + } + $rows[] = phutil_tag( 'tr', $row_attributes, array( + $blame_cells, phutil_tag( 'th', array( @@ -119,7 +173,7 @@ final class PhabricatorSourceCodeView extends AphrontView { array( 'class' => 'phabricator-source-code', ), - $content_line), + $line), )); $line_number++; @@ -130,15 +184,24 @@ final class PhabricatorSourceCodeView extends AphrontView { $classes[] = 'remarkup-code'; $classes[] = 'PhabricatorMonospaced'; + $symbol_metadata = $this->getSymbolMetadata(); + + $sigils = array(); + $sigils[] = 'phabricator-source'; + $sigils[] = 'has-symbols'; + + Javelin::initBehavior('repository-crossreference'); + return phutil_tag_div( 'phabricator-source-code-container', javelin_tag( 'table', array( 'class' => implode(' ', $classes), - 'sigil' => 'phabricator-source', + 'sigil' => implode(' ', $sigils), 'meta' => array( 'uri' => (string)$this->uri, + 'symbols' => $symbol_metadata, ), ), phutil_implode_html('', $rows))); diff --git a/webroot/rsrc/css/aphront/phabricator-nav-view.css b/webroot/rsrc/css/aphront/phabricator-nav-view.css index 6dbd0931a6..ce9fffa101 100644 --- a/webroot/rsrc/css/aphront/phabricator-nav-view.css +++ b/webroot/rsrc/css/aphront/phabricator-nav-view.css @@ -83,7 +83,10 @@ .device-desktop .phui-navigation-shell .has-drag-nav .phabricator-nav-local { width: 310px; padding: 0; - background: transparent; + + /* See PHI568. If we don't paint the background explicitly, the content can + render underneath it when scrolled horizontally. */ + background: {$page.background}; } .device-phone .phabricator-side-menu-home .phabricator-nav-content { diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css index 194561aa7a..b9683dc7c6 100644 --- a/webroot/rsrc/css/application/differential/changeset-view.css +++ b/webroot/rsrc/css/application/differential/changeset-view.css @@ -165,10 +165,6 @@ padding: 0; } -.diffusion-source td.cov { - padding: 0 8px; -} - td.cov-U { background: #dd8866; } diff --git a/webroot/rsrc/css/application/diffusion/diffusion-source.css b/webroot/rsrc/css/application/diffusion/diffusion-source.css deleted file mode 100644 index a2c67cf16d..0000000000 --- a/webroot/rsrc/css/application/diffusion/diffusion-source.css +++ /dev/null @@ -1,102 +0,0 @@ -/** - * @provides diffusion-source-css - */ - -.diffusion-source { - width: 100%; - background: {$page.content}; - overflow: hidden; -} - -.device-phone .diffusion-source-wrap { - overflow: scroll; - -webkit-overflow-scrolling: touch; -} - -.diffusion-source tr.phabricator-source-highlight { - background: {$sh-yellowbackground}; -} - -.diffusion-source th { - text-align: right; - vertical-align: top; - background: {$lightgreybackground}; - color: {$bluetext}; - border-right: 1px solid {$thinblueborder}; -} - -.diffusion-source td { - vertical-align: top; - white-space: pre-wrap; - padding-top: 1px; - padding-bottom: 1px; - padding-left: 8px; - width: 100%; - word-break: break-all; -} - -.device .diffusion-source td { - word-break: normal; - white-space: nowrap; -} - -.diffusion-browse-type-form { - float: right; -} - -.diffusion-blame-link, -.diffusion-rev-link { - white-space: nowrap; -} - -.diffusion-blame-link { - min-width: 28px; -} - -.diffusion-source th.diffusion-rev-link { - text-align: left; - min-width: 130px; -} - -.diffusion-blame-link a, -.diffusion-rev-link a, -.diffusion-line-link a { - color: {$darkbluetext}; -} - -.diffusion-rev-link a { - margin: 0 8px 0 0; - display: inline-block; -} - -.diffusion-rev-link span { - display: inline-block; - margin-right: 4px; - margin-left: -4px; - color: {$lightgreytext}; -} - -.diffusion-blame-link a, -.diffusion-line-link a { - /* Give the user a larger click target. */ - display: block; - padding: 2px 8px; -} - -.diffusion-line-link { - -moz-user-select: -moz-none; - -khtml-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.diffusion-rev-link .diffusion-author-link { - display: inline-block; - padding: 0; - margin: 2px 6px -4px 8px; - width: 16px; - height: 16px; - background-size: 100% 100%; - background-repeat: no-repeat; -} diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css index 990c6f0bab..686ec632dc 100644 --- a/webroot/rsrc/css/core/remarkup.css +++ b/webroot/rsrc/css/core/remarkup.css @@ -31,9 +31,10 @@ font-size: 13px; font-weight: bold; background: rgba({$alphablue},0.08); - display: inline-block; + display: table-cell; border-top-left-radius: 3px; border-top-right-radius: 3px; + overflow: hidden; } .phabricator-remarkup .code-block-counterexample .remarkup-code-header { diff --git a/webroot/rsrc/css/core/syntax.css b/webroot/rsrc/css/core/syntax.css index a0b84ea2b3..cfc82da09b 100644 --- a/webroot/rsrc/css/core/syntax.css +++ b/webroot/rsrc/css/core/syntax.css @@ -6,11 +6,6 @@ color: #aa0066; } -.remarkup-code .over-the-line { - color: #aa0066; - margin-right: 1px; -} - .remarkup-code td > span { display: inline; word-break: break-all; @@ -24,11 +19,9 @@ .remarkup-code .rbw_i { color: indigo; } .remarkup-code .rbw_v { color: violet; } -.repository-crossreference .remarkup-code .crossreference-item { - background: lightyellow; - border-bottom: 1px dotted #bbddbb; -} -.crossreference-cursor { +span.crossreference-item { + background: {$lightyellow}; + border-bottom: 1px solid {$yellow}; cursor: help; } diff --git a/webroot/rsrc/css/layout/phabricator-source-code-view.css b/webroot/rsrc/css/layout/phabricator-source-code-view.css index 1836d321a5..6f2864d067 100644 --- a/webroot/rsrc/css/layout/phabricator-source-code-view.css +++ b/webroot/rsrc/css/layout/phabricator-source-code-view.css @@ -7,7 +7,6 @@ overflow-y: hidden; border: 1px solid {$paste.border}; border-radius: 3px; - background-color: {$paste.content}; } .phui-oi .phabricator-source-code-container { @@ -18,6 +17,7 @@ white-space: pre-wrap; padding: 2px 8px 1px; width: 100%; + background: #ffffff; } .phabricator-source-line { @@ -25,16 +25,11 @@ text-align: right; border-right: 1px solid {$paste.border}; color: {$sh-yellowtext}; +} - /* When the user selects rows of source, don't visibly select the line - numbers beside them. We use JS to strip the line numbers out when the user - copies the text. */ - -moz-user-select: -moz-none; - -khtml-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; - white-space: nowrap; +.phabricator-source-line > a::before { + /* Render the line numbers as a pseudo-element so they don't get copied. */ + content: attr(data-n); } th.phabricator-source-line a, @@ -52,10 +47,14 @@ th.phabricator-source-line a:hover { text-decoration: none; } -.phabricator-source-highlight { +.phabricator-source-highlight .phabricator-source-code { background: {$paste.highlight}; } +.phabricator-source-highlight .phabricator-source-line { + background: {$paste.border}; +} + .phabricator-source-code-summary { padding-bottom: 8px; } @@ -69,3 +68,58 @@ th.phabricator-source-line a:hover { .phabricator-source-code-summary .phabricator-source-code { white-space: nowrap; } + + +.phabricator-source-blame-skip, +.phabricator-source-blame-info { + -moz-user-select: -moz-none; + -khtml-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.phabricator-source-blame-skip { + min-width: 28px; + border-right: 1px solid {$thinblueborder}; +} + +.phabricator-source-blame-info { + white-space: nowrap; + min-width: 130px; + border-right: 1px solid {$paste.border}; + padding-right: 8px; + + vertical-align: middle; + color: #ffffff; +} + +.phabricator-source-blame-info a { + color: {$darkbluetext}; + text-shadow: 1px 1px rgba(0, 0, 0, 0.111); +} + +.phabricator-source-blame-skip a { + /* Give the user a larger click target. */ + display: block; + padding: 2px 8px; +} + +.phabricator-source-blame-skip a .phui-icon-view { + color: {$darkbluetext}; +} + +.device-desktop .phabricator-source-blame-skip a:hover { + background: {$bluebackground}; +} + +.phabricator-source-blame-author { + display: inline-block; + vertical-align: middle; + padding: 0; + margin: 0 6px 0 8px; + width: 16px; + height: 16px; + background-size: 100% 100%; + background-repeat: no-repeat; +} diff --git a/webroot/rsrc/js/application/diffusion/behavior-jump-to.js b/webroot/rsrc/js/application/diffusion/behavior-jump-to.js deleted file mode 100644 index 96b4947e02..0000000000 --- a/webroot/rsrc/js/application/diffusion/behavior-jump-to.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @provides javelin-behavior-diffusion-jump-to - * @requires javelin-behavior - * javelin-vector - * javelin-dom - */ - -JX.behavior('diffusion-jump-to', function(config) { - - setTimeout(function() { - var pos = JX.Vector.getPosWithScroll(JX.$(config.target)); - JX.DOM.scrollToPosition(0, pos.y - 100); - }, 0); - -}); diff --git a/webroot/rsrc/js/application/diffusion/behavior-load-blame.js b/webroot/rsrc/js/application/diffusion/behavior-load-blame.js deleted file mode 100644 index bebafc8886..0000000000 --- a/webroot/rsrc/js/application/diffusion/behavior-load-blame.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @provides javelin-behavior-load-blame - * @requires javelin-behavior - * javelin-dom - * javelin-request - */ - -JX.behavior('load-blame', function(config) { - - new JX.Request(location.href, function (response) { - JX.DOM.setContent(JX.$(config.id), JX.$H(response)); - }).send(); - -}); diff --git a/webroot/rsrc/js/application/files/behavior-document-engine.js b/webroot/rsrc/js/application/files/behavior-document-engine.js index ccbd41ca92..2fc4887408 100644 --- a/webroot/rsrc/js/application/files/behavior-document-engine.js +++ b/webroot/rsrc/js/application/files/behavior-document-engine.js @@ -7,8 +7,6 @@ JX.behavior('document-engine', function(config, statics) { - - function onmenu(e) { var node = e.getNode('document-engine-view-dropdown'); var data = JX.Stratcom.getData(node); @@ -153,7 +151,7 @@ JX.behavior('document-engine', function(config, statics) { } data.sequence = (data.sequence || 0) + 1; - var handler = JX.bind(null, onrender, data, data.sequence); + var handler = JX.bind(null, onrender, data, data.sequence, spec); data.viewKey = spec.viewKey; @@ -192,7 +190,7 @@ JX.behavior('document-engine', function(config, statics) { JX.DOM.setContent(viewport, JX.$H(spec.loadingMarkup)); } - function onrender(data, sequence, r) { + function onrender(data, sequence, spec, r) { // If this isn't the most recent request we sent, throw it away. This can // happen if the user makes multiple selections from the menu while we are // still rendering the first view. @@ -211,6 +209,152 @@ JX.behavior('document-engine', function(config, statics) { data.loadingView = false; JX.DOM.setContent(viewport, JX.$H(r.markup)); + + // If this engine supports rendering blame, populate or draw it. + if (spec.canBlame) { + blame(data); + } + } + + function blame(data) { + // If the rendering engine can't handle blame, bail. + if (!data.blame.uri) { + return; + } + + // If we already have an outstanding request for blame data, bail. + if (data.blame.request) { + return; + } + + // If we don't have blame data yet, request it and then try rendering + // again later. + if (!data.blame.value) { + var req = new JX.Request(data.blame.uri, JX.bind(null, onblame, data)); + data.blame.request = req; + req.send(); + return; + } + + // We're ready to render. + var viewport = JX.$(data.viewportID); + + var row_nodes = JX.DOM.scry(viewport, 'tr'); + var row_list = []; + var ii; + + for (ii = 0; ii < row_nodes.length; ii++) { + var row = {}; + var keep = false; + var node = row_nodes[ii]; + + for (var jj = 0; jj < node.childNodes.length; jj++) { + var child = node.childNodes[jj]; + + if (!JX.DOM.isType(child, 'th')) { + continue; + } + + var spec = child.getAttribute('data-blame'); + if (spec) { + row[spec] = child; + keep = true; + } + + if (spec === 'info') { + row.lines = child.getAttribute('data-blame-lines'); + } + } + + if (keep) { + row_list.push(row); + } + } + + var last = null; + for (ii = 0; ii < row_list.length; ii++) { + var commit = data.blame.value.blame[row_list[ii].lines - 1]; + row_list[ii].commit = commit; + row_list[ii].last = last; + last = commit; + } + + for (ii = 0; ii < row_list.length; ii++) { + renderBlame(row_list[ii], data.blame.value); + } + } + + function onblame(data, r) { + data.blame.request = null; + data.blame.value = r; + blame(data); + } + + function renderBlame(row, blame) { + var spec = blame.map[row.commit]; + + var info = null; + var skip = null; + + if (spec && (row.commit != row.last)) { + skip = JX.$H(spec.skip); + info = JX.$H(spec.info); + } + + if (row.skip) { + JX.DOM.setContent(row.skip, skip); + } + + if (row.info) { + JX.DOM.setContent(row.info, info); + } + + var epoch_range = (blame.epoch.max - blame.epoch.min); + + var epoch_value; + if (!epoch_range) { + epoch_value = 1; + } else { + epoch_value = (spec.epoch - blame.epoch.min) / epoch_range; + } + + var h_min = 0.04; + var h_max = 0.44; + var h = h_min + ((h_max - h_min) * epoch_value); + + var s = 0.44; + + var v_min = 0.92; + var v_max = 1.00; + var v = v_min + ((v_max - v_min) * epoch_value); + + row.info.style.background = getHSV(h, s, v); + } + + function getHSV(h, s, v) { + var r, g, b, i, f, p, q, t; + + i = Math.floor(h * 6); + f = h * 6 - i; + p = v * (1 - s); + q = v * (1 - f * s); + t = v * (1 - (1 - f) * s); + + switch (i % 6) { + case 0: r = v, g = t, b = p; break; + case 1: r = q, g = v, b = p; break; + case 2: r = p, g = v, b = t; break; + case 3: r = p, g = q, b = v; break; + case 4: r = t, g = p, b = v; break; + case 5: r = v, g = p, b = q; break; + } + + r = Math.round(r * 255); + g = Math.round(g * 255); + b = Math.round(b * 255); + + + return 'rgb(' + r + ', ' + g + ', ' + b + ')'; } if (!statics.initialized) { @@ -218,10 +362,18 @@ JX.behavior('document-engine', function(config, statics) { statics.initialized = true; } - if (config && config.renderControlID) { - var control = JX.$(config.renderControlID); + if (config && config.controlID) { + var control = JX.$(config.controlID); var data = JX.Stratcom.getData(control); - onview(data, null, true); + + switch (config.next) { + case 'render': + onview(data, null, true); + break; + case 'blame': + blame(data); + break; + } } }); diff --git a/webroot/rsrc/js/application/repository/repository-crossreference.js b/webroot/rsrc/js/application/repository/repository-crossreference.js index 5f2f8306bf..548ef6173b 100644 --- a/webroot/rsrc/js/application/repository/repository-crossreference.js +++ b/webroot/rsrc/js/application/repository/repository-crossreference.js @@ -11,12 +11,29 @@ JX.behavior('repository-crossreference', function(config, statics) { var highlighted; var linked = []; - var isMac = navigator.platform.indexOf('Mac') > -1; - var signalKey = isMac ? 91 /*COMMAND*/ : 17 /*CTRL*/; - function isSignalkey(event) { - return isMac ? - event.getRawEvent().metaKey : - event.getRawEvent().ctrlKey; + function isMacOS() { + return (navigator.platform.indexOf('Mac') > -1); + } + + function isHighlightModifierKey(e) { + var signal_key; + if (isMacOS()) { + // On macOS, use the "Command" key. + signal_key = 91; + } else { + // On other platforms, use the "Control" key. + signal_key = 17; + } + + return (e.getRawEvent().keyCode === signal_key); + } + + function hasHighlightModifierKey(e) { + if (isMacOS()) { + return e.getRawEvent().metaKey; + } else { + return e.getRawEvent().ctrlKey; + } } var classHighlight = 'crossreference-item'; @@ -43,7 +60,7 @@ JX.behavior('repository-crossreference', function(config, statics) { unhighlight(); return; } - if (!isSignalkey(e)) { + if (!hasHighlightModifierKey(e)) { return; } @@ -76,7 +93,7 @@ JX.behavior('repository-crossreference', function(config, statics) { target = target.parentNode; } } else if (e.getType() === 'click') { - openSearch(target, lang); + openSearch(target, {lang: lang}); } }); } @@ -85,13 +102,24 @@ JX.behavior('repository-crossreference', function(config, statics) { highlighted = null; } - function openSearch(target, lang) { + function openSearch(target, context) { var symbol = target.textContent || target.innerText; - var query = { - lang : lang, - repositories : config.repositories.join(','), - jump : true - }; + + context = context || {}; + context.lang = context.lang || null; + context.repositories = + context.repositories || + (config && config.repositories) || + []; + + var query = JX.copy({}, context); + if (query.repositories.length) { + query.repositories = query.repositories.join(','); + } else { + delete query.repositories; + } + query.jump = true; + var c = target.className; c = c.replace(classHighlight, '').trim(); @@ -112,9 +140,11 @@ JX.behavior('repository-crossreference', function(config, statics) { query.line = line; } - var path = getPath(target); - if (path !== null) { - query.path = path; + if (!query.hasOwnProperty('path')) { + var path = getPath(target); + if (path !== null) { + query.path = path; + } } var char = getChar(target); @@ -124,7 +154,8 @@ JX.behavior('repository-crossreference', function(config, statics) { var uri = JX.$U('/diffusion/symbol/' + symbol + '/'); uri.addQueryParams(query); - window.open(uri); + + window.open(uri.toString()); } function linkAll() { @@ -188,16 +219,6 @@ JX.behavior('repository-crossreference', function(config, statics) { // Ignore. } - // This method works in Diffusion, when viewing the content of a file at - // a particular commit. - var file; - try { - file = JX.DOM.findAbove(target, 'div', 'diffusion-file-content-view'); - return JX.Stratcom.getData(file).path; - } catch (ex) { - // Ignore. - } - return null; } @@ -227,12 +248,6 @@ JX.behavior('repository-crossreference', function(config, statics) { return null; } - if (config.container) { - link(JX.$(config.container), config.lang); - } else if (config.section) { - linkAll(JX.$(config.section)); - } - JX.Stratcom.listen( 'differential-preview-update', null, @@ -245,9 +260,10 @@ JX.behavior('repository-crossreference', function(config, statics) { ['keydown', 'keyup'], null, function(e) { - if (e.getRawEvent().keyCode !== signalKey) { + if (!isHighlightModifierKey(e)) { return; } + setCursorMode(e.getType() === 'keydown'); if (!statics.active) { @@ -272,4 +288,68 @@ JX.behavior('repository-crossreference', function(config, statics) { JX.DOM.alterClass(element, classMouseCursor, statics.active); }); } + + + if (config && config.container) { + link(JX.$(config.container), config.lang); + } + + JX.Stratcom.listen( + ['mouseover', 'mouseout', 'click'], + ['has-symbols', 'tag:span'], + function(e) { + var type = e.getType(); + + if (type === 'mouseout') { + unhighlight(); + return; + } + + if (!hasHighlightModifierKey(e)) { + return; + } + + var target = e.getTarget(); + + try { + // If we're in an inline comment, don't link symbols. + if (JX.DOM.findAbove(target, 'div', 'differential-inline-comment')) { + return; + } + } catch (ex) { + // Continue if we're not inside an inline comment. + } + + // If only part of the symbol was edited, the symbol name itself will + // have another "" inside of it which highlights only the + // edited part. Skip over it. + if (JX.DOM.isNode(target, 'span') && (target.className === 'bright')) { + target = target.parentNode; + } + + if (type === 'click') { + openSearch(target, e.getNodeData('has-symbols').symbols); + e.kill(); + return; + } + + if (e.getType() === 'mouseover') { + while (target && target !== document.body) { + if (!JX.DOM.isNode(target, 'span')) { + target = target.parentNode; + continue; + } + + if (!class_map.hasOwnProperty(target.className)) { + target = target.parentNode; + continue; + } + + highlighted = target; + JX.DOM.alterClass(highlighted, classHighlight, true); + break; + } + } + }); + }); diff --git a/webroot/rsrc/js/core/behavior-line-linker.js b/webroot/rsrc/js/core/behavior-line-linker.js index 8a68cec842..8cb3d0c615 100644 --- a/webroot/rsrc/js/core/behavior-line-linker.js +++ b/webroot/rsrc/js/core/behavior-line-linker.js @@ -19,9 +19,7 @@ JX.behavior('phabricator-line-linker', function() { // Ignore. } - function getRowNumber(tr) { - var th = tr.firstChild; - + function getRowNumber(th) { // If the "" tag contains an "" with "data-n" that we're using // to prevent copy/paste of line numbers, use that. if (th.firstChild) { @@ -31,12 +29,12 @@ JX.behavior('phabricator-line-linker', function() { } } - return +(th.textContent || th.innerText); + return null; } JX.Stratcom.listen( ['click', 'mousedown'], - ['phabricator-source', 'tag:tr', 'tag:th', 'tag:a'], + ['phabricator-source', 'tag:th', 'tag:a'], function(e) { if (!e.isNormalMouseEvent()) { return; @@ -47,13 +45,13 @@ JX.behavior('phabricator-line-linker', function() { // table. The row's immediate ancestor table needs to be the table with // the "phabricator-source" sigil. - var row = e.getNode('tag:tr'); + var cell = e.getNode('tag:th'); var table = e.getNode('phabricator-source'); - if (JX.DOM.findAbove(row, 'table') !== table) { + if (JX.DOM.findAbove(cell, 'table') !== table) { return; } - var number = getRowNumber(row); + var number = getRowNumber(cell); if (!number) { return; } @@ -66,7 +64,7 @@ JX.behavior('phabricator-line-linker', function() { return; } - origin = row; + origin = cell; target = origin; root = table; @@ -80,7 +78,7 @@ JX.behavior('phabricator-line-linker', function() { if (e.getNode('phabricator-source') !== root) { return; } - target = e.getNode('tag:tr'); + target = e.getNode('tag:th'); var min; var max; @@ -115,6 +113,9 @@ JX.behavior('phabricator-line-linker', function() { highlighted = []; // Highlight the newly selected rows. + min = JX.DOM.findAbove(min, 'tr'); + max = JX.DOM.findAbove(max, 'tr'); + var cursor = min; while (true) { JX.DOM.alterClass(cursor, 'phabricator-source-highlight', true); @@ -144,9 +145,14 @@ JX.behavior('phabricator-line-linker', function() { var o = getRowNumber(origin); var t = getRowNumber(target); var uri = JX.Stratcom.getData(root).uri; + var path; if (!uri) { - uri = ('' + window.location).split('$')[0]; + uri = JX.$U(window.location); + path = uri.getPath(); + path = path.replace(/\$[\d-]+$/, ''); + uri.setPath(path); + uri = uri.toString(); } origin = null; @@ -154,7 +160,11 @@ JX.behavior('phabricator-line-linker', function() { root = null; var lines = (o == t ? o : Math.min(o, t) + '-' + Math.max(o, t)); - uri = uri + '$' + lines; + + uri = JX.$U(uri); + path = uri.getPath(); + path = path + '$' + lines; + uri = uri.setPath(path).toString(); JX.History.replace(uri); @@ -166,4 +176,16 @@ JX.behavior('phabricator-line-linker', function() { } }); + + // Try to jump to the highlighted lines if we don't have an explicit anchor + // in the URI. + if (!window.location.hash.length) { + try { + var anchor = JX.$('phabricator-line-linker-anchor'); + JX.DOM.scrollToPosition(0, JX.$V(anchor).y - 60); + } catch (ex) { + // If we didn't hit an element on the page, just move on. + } + } + }); diff --git a/webroot/rsrc/js/core/behavior-phabricator-nav.js b/webroot/rsrc/js/core/behavior-phabricator-nav.js index 50d8e1c820..cf6dc880bc 100644 --- a/webroot/rsrc/js/core/behavior-phabricator-nav.js +++ b/webroot/rsrc/js/core/behavior-phabricator-nav.js @@ -49,7 +49,7 @@ JX.behavior('phabricator-nav', function(config) { { element: drag, parameter: 'left', - start: JX.$V(drag).x + start: get_width() }, { element: content, @@ -102,15 +102,24 @@ JX.behavior('phabricator-nav', function(config) { .setData( { key: 'filetree.width', - value: JX.$V(drag).x + value: get_width() }) .send(); }); + function get_width() { + // See PHI568. If the document has scrolled horizontally, the "x" position + // of the bar will be the actual width of the menu plus the horizontal + // scroll position (because the element is "position: fixed"). Subtract the + // document scroll position when saving the element width so that scrolling + // to the right and then toggling the filetree UI does not make it grow + // any wider. + return (JX.$V(drag).x - JX.Vector.getScroll().x); + } var saved_width = config.width; function savedrag() { - saved_width = JX.$V(drag).x; + saved_width = get_width(); local.style.width = ''; drag.style.left = '';